Add generic file attachments to chat messages

Messages can now carry an arbitrary file (MessageFile), parallel to the
existing MessageImage feature rather than a refactor of it. Files serve
with Content-Disposition: attachment to force a download and prevent an
uploaded HTML/SVG from executing same-origin. No content-type allowlist,
same 8MB cap as images for now (a separate size-limit redesign is tracked
as its own issue).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
ksmithandClaude Sonnet 5 committed 2026-08-15 08:02:55 -06:00
1 parent 89be497fdb
commit c78d7454b6
21 files changed
+697 -55

No files matched your search

+43 -1
View File
@@ -1,5 +1,13 @@
import { apiFetch, ApiError, NetworkError } from './client'
import type { Message, MyRoomItem, Room, RoomListItem, RoomMember, RoomRole } from '../types'
import type {
Message,
MessageFileInfo,
MyRoomItem,
Room,
RoomListItem,
RoomMember,
RoomRole,
} from '../types'
export function listRooms(): Promise<RoomListItem[]> {
return apiFetch<RoomListItem[]>('/api/rooms')
@@ -114,3 +122,37 @@ export async function uploadRoomImage(roomId: string, file: File): Promise<{ id:
export function getRoomImageUrl(roomId: string, imageId: string): string {
return `/api/rooms/${roomId}/images/${imageId}`
}
// Not apiFetch, same multipart-boundary reason as uploadRoomImage.
export async function uploadRoomFile(roomId: string, file: File): Promise<MessageFileInfo> {
const formData = new FormData()
formData.append('file', file)
let response: Response
try {
response = await fetch(`/api/rooms/${roomId}/files`, {
method: 'POST',
credentials: 'include',
body: formData,
})
} catch {
throw new NetworkError()
}
if (!response.ok) {
let detail = response.statusText
try {
const body = await response.json()
detail = body.detail ?? detail
} catch {
// response had no JSON body
}
throw new ApiError(response.status, detail)
}
return (await response.json()) as MessageFileInfo
}
export function getRoomFileUrl(roomId: string, fileId: string): string {
return `/api/rooms/${roomId}/files/${fileId}`
}