diff --git a/docs/API-REFERENCE.md b/docs/API-REFERENCE.md index 797bb95f..78938d65 100644 --- a/docs/API-REFERENCE.md +++ b/docs/API-REFERENCE.md @@ -378,7 +378,7 @@ Validate a session token. ### `POST /api/upload` -Upload a file (images or markdown, max 20MB). +Upload a file (images, markdown or PDF, max 20MB). Binary types are checked against their magic bytes. **Content-Type:** `multipart/form-data` **Form field:** `file` diff --git a/server/upload-routes.test.ts b/server/upload-routes.test.ts index 468dfe2b..5b60d193 100644 --- a/server/upload-routes.test.ts +++ b/server/upload-routes.test.ts @@ -310,6 +310,21 @@ describe('POST /api/upload — magic-byte validation (M2)', () => { expect(body.success).toBe(true) }) + it('accepts a valid PDF file', async () => { + const pdf = Buffer.from('%PDF-1.4\n1 0 obj\n<<>>\nendobj\ntrailer\n<<>>\n%%EOF\n') + const res = await uploadFile('doc.pdf', 'application/pdf', pdf) + expect(res.status).toBe(200) + const body = await res.json() as { success: boolean } + expect(body.success).toBe(true) + }) + + it('rejects a .pdf whose bytes are not a PDF (signature mismatch)', async () => { + const res = await uploadFile('fake.pdf', 'application/pdf', VALID_PNG) + expect(res.status).toBe(400) + const body = await res.json() as { error: string } + expect(body.error).toContain('signature mismatch') + }) + it('rejects a file with unknown binary signature claiming to be PNG', async () => { const garbage = Buffer.from('not a real image file at all') const res = await uploadFile('bad.png', 'image/png', garbage) diff --git a/server/upload-routes.ts b/server/upload-routes.ts index e1f16b99..d8329dd2 100644 --- a/server/upload-routes.ts +++ b/server/upload-routes.ts @@ -328,9 +328,10 @@ export function createUploadRouter( '.gif': ['image/gif'], '.webp': ['image/webp'], '.md': ['text/markdown'], + '.pdf': ['application/pdf'], } /** Binary MIME types that have detectable file signatures (magic bytes). */ - const BINARY_MIME_TYPES = new Set(['image/png', 'image/jpeg', 'image/gif', 'image/webp']) + const BINARY_MIME_TYPES = new Set(['image/png', 'image/jpeg', 'image/gif', 'image/webp', 'application/pdf']) const upload = multer({ storage, limits: { fileSize: 20 * 1024 * 1024 }, diff --git a/src/components/DropZone.tsx b/src/components/DropZone.tsx index 4c77893e..13717a0e 100644 --- a/src/components/DropZone.tsx +++ b/src/components/DropZone.tsx @@ -93,7 +93,7 @@ export function DropZone({ onUpload, disabled }: Props) {
-

Drop images or markdown here

+

Drop images, markdown or PDFs here

) diff --git a/src/components/InputBar.tsx b/src/components/InputBar.tsx index 6fa4dd54..98153661 100644 --- a/src/components/InputBar.tsx +++ b/src/components/InputBar.tsx @@ -58,11 +58,11 @@ function ToolbarAction({ onClick, disabled, title, accent = false, children }: { /** * Attach-files button. A `+` rather than a paperclip, matching the convention * other LLM composers have settled on; the title still says what it takes, - * since the picker only accepts images and markdown. + * since the picker only accepts images, markdown and PDFs. */ function AttachButton({ onClick, disabled, accent = false }: { onClick: () => void; disabled: boolean; accent?: boolean }) { return ( - + ) @@ -773,7 +773,7 @@ export const InputBar = forwardRef(function Input ref={fileInputRef} type="file" multiple - accept="image/png,image/jpeg,image/gif,image/webp,text/markdown,.md" + accept="image/png,image/jpeg,image/gif,image/webp,text/markdown,.md,application/pdf,.pdf" onChange={handleFileChange} className="hidden" />