/** * Attachment types shared between api and web (issue #27, ADR 0011). M2 * accepted images only; M6 (#61) adds a configurable allowlist for general * attachments (PDF, office files, …) plus an SVG policy. */ export const ATTACHMENT_IMAGE_MIME_TYPES = [ 'image/png', 'image/jpeg', 'image/gif', 'image/webp', ] as const; export type AttachmentMimeType = (typeof ATTACHMENT_IMAGE_MIME_TYPES)[number]; export function isImageMimeType(mimeType: string): boolean { return (ATTACHMENT_IMAGE_MIME_TYPES as readonly string[]).includes(mimeType); } /** * SVG is an image but also an XML document that can carry scripts and event * handlers, so it is never treated like a raster image: it is sanitized or * rejected on upload (instance setting) and always served as a download, * never inline (security.md §Uploads). */ export const SVG_MIME_TYPE = 'image/svg+xml'; /** How the instance handles SVG uploads (ADR 0011, security.md §Uploads). */ export type SvgPolicy = 'reject' | 'sanitize'; /** * Extension → served MIME type for the non-image allowlist. The allowlist is * keyed on the lowercase extension (what an admin configures and what names * the download); the MIME here only sets the response `Content-Type`, and * non-images are always sent with `Content-Disposition: attachment` + * `nosniff`, so a wrong guess can never cause inline execution. */ export const ATTACHMENT_EXTENSION_MIME_TYPES: Readonly> = { pdf: 'application/pdf', txt: 'text/plain', md: 'text/markdown', csv: 'text/csv', rtf: 'application/rtf', doc: 'application/msword', docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', odt: 'application/vnd.oasis.opendocument.text', xls: 'application/vnd.ms-excel', xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', ods: 'application/vnd.oasis.opendocument.spreadsheet', ppt: 'application/vnd.ms-powerpoint', pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation', odp: 'application/vnd.oasis.opendocument.presentation', zip: 'application/zip', }; /** * Default non-image allowlist (extensions, lowercase, no dot). Images from * {@link ATTACHMENT_IMAGE_MIME_TYPES} are always allowed regardless of this * list; SVG is governed separately by the SVG policy. */ export const DEFAULT_ATTACHMENT_EXTENSIONS: readonly string[] = [ 'pdf', 'txt', 'md', 'csv', 'doc', 'docx', 'odt', 'xls', 'xlsx', 'ods', 'ppt', 'pptx', 'odp', 'zip', ]; /** Lowercase extension without the leading dot, or '' when the name has none. */ export function fileExtension(fileName: string): string { const dot = fileName.lastIndexOf('.'); if (dot <= 0 || dot === fileName.length - 1) return ''; return fileName.slice(dot + 1).toLowerCase(); } /** * Hard ceiling on the raw multipart body the api will buffer in memory, * independent of the per-pond/user `max_file_bytes` quota (QuotaService) * that governs the actually accepted size — mirrors how * `MAX_PAGE_DOCUMENT_BYTES` relates to the JSON body-parser limit (pages.ts). */ export const MAX_UPLOAD_PARSE_BYTES = 64 * 1024 * 1024; export interface AttachmentView { id: string; pondId: string; pageId: string | null; fileName: string; mimeType: string; sizeBytes: number; createdAt: string; } /** * A row in the page-attachments section and the pond file manager (#61): * carries the uploader's display name and, for the pond manager, the title * of the page currently referencing the file (null = orphan candidate). */ export interface AttachmentListItemView extends AttachmentView { uploaderName: string; pageTitle: string | null; } /** Pond-wide file manager payload (Pond Admin, #61). */ export interface PondFilesView { files: AttachmentListItemView[]; storageBytesUsed: number; storageBytesLimit: number; }