import type { IncomingMessage as HttpRequest, ServerResponse } from "node:http"; import type { ApiContext } from "./api.js"; import { type ParsedRoute } from "./route-helpers.js"; export declare function ensureFilesDir(): void; /** Strip any directory components from an uploaded filename (path-traversal guard). */ export declare function sanitizeUploadFilename(name: string): string; /** Restrict a sessionId to a safe single path segment (no separators / traversal). */ export declare function sanitizeSessionId(id: string): string; /** Date-bucketed, session-scoped upload directory under UPLOADS_DIR. */ export declare function uploadDir(sessionId: string, date?: string): string; /** True only when absPath resolves inside FILES_DIR or UPLOADS_DIR (no arbitrary reads). */ export declare function isServablePath(absPath: string): boolean; export declare function resolveCustomUploadPath(requestedPath: string | null | undefined): string | null; export declare function allowUploadedFileOpen(context: Pick): boolean; /** Delete date-bucket directories under UPLOADS_DIR older than maxAgeDays. Returns count removed. */ export declare function cleanupOldUploads(maxAgeDays?: number): number; export declare function mimeFromFilename(filename: string): string; export declare function expandPath(p: string): string; /** Max bytes we'll read into memory for inline display. Larger files → tooLarge flag. */ export declare const MAX_READ_SIZE: number; /** * Build the candidate absolute path for a managed file read. Only relative * paths under `files/` or `uploads/` produce a candidate; absolute, home, * project, cwd, traversal, backslash, and other-root paths produce none. */ export declare function readPathCandidates(requestedPath: string): string[]; /** * Resolve a requested path to the first candidate that exists as a regular file. * Returns { resolvedPath: null, candidates } when none exist. */ export declare function resolveReadPath(requestedPath: string): { resolvedPath: string | null; candidates: string[]; error?: string; status?: 400 | 403; }; export interface FileReadAssessment { allowed: boolean; reason?: string; } export declare function assessFileRead(absPath: string, _opts?: { authenticated?: boolean; }): FileReadAssessment; export interface FileClassification { mime: string; size: number; /** true when the file is over MAX_READ_SIZE — content is NOT read. */ tooLarge: boolean; /** true when detected as binary (by MIME or NUL byte) — content is NOT returned. */ binary: boolean; /** utf-8 text content; only present for non-binary, non-too-large files. */ content?: string; } interface ManagedFileRead { resolvedPath: string; classification: FileClassification; } interface ManagedFileReadError { status: 400 | 403 | 404 | 500; error: string; } /** * Open, authorize, and read a managed path through ONE file descriptor. * * Security invariant: authorization and byte-read are tied to the same opened * inode. The leaf is opened with O_NOFOLLOW, the opened fd's real path is checked * against the selected managed root, bytes are read from that fd, and a final * path-stability check refuses a swap that happened during the read. The route * never re-opens the authorized path string for content. */ export declare function readManagedFile(requestedPath: string): ManagedFileRead | ManagedFileReadError; /** * Classify an existing file: size cap → binary detection → text read. * Caller must guarantee absPath is a regular file. Pure-ish (touches disk * read-only); unit-tested against temp files. */ export declare function classifyFile(absPath: string): FileClassification; export type LocalFileIngestion = { ok: true; buffer: Buffer; realPath: string; } | { ok: false; status: 400 | 403 | 404 | 413; error: string; }; /** * Read a caller-named local file for ingestion (e.g. JSON-path attachment * uploads) under the standing file-read policy. Symlink-swap-proof: the source * is canonicalized and opened ONCE (O_NOFOLLOW on the canonical path), the * assessment runs against that opened real path, the size cap uses fstat on * the SAME descriptor, and the bytes are read from that descriptor — a path * swapped between checks is detected by inode comparison and refused. */ export declare function readLocalFileForIngestion(requestedPath: string, maxBytes: number): LocalFileIngestion; export interface MultipartFileUpload { filename: string; buffer: Buffer; fields: Record; /** True when the file hit the size limit and was cut off — reject the upload. */ truncated: boolean; } /** A refused multipart request — carries the HTTP status the route should emit. */ export declare class MultipartUploadError extends Error { readonly status: 400 | 413; constructor(status: 400 | 413, message: string); } /** * Parse a single-file multipart request into memory — the shared upload * machinery consumers outside this module (e.g. work-item attachments) reuse * instead of wiring Busboy themselves. Hardened (Todos v2 slice-5 review F3): * exactly ONE file part, and it must be named "file"; strict Busboy * files/fields/parts/fieldSize limits; every limit event and unexpected part * is a refusal; and an aggregate request-byte ceiling * (maxFileSize + overhead) is enforced up front from Content-Length AND while * streaming, so many individually-valid parts cannot accumulate memory. On * refusal all buffered chunks are dropped immediately. */ export declare function readMultipartFile(req: HttpRequest, maxFileSize: number): Promise; export { fileIdsToMedia } from "./message-media.js"; /** * Re-home first-message attachments: files uploaded before a session existed land * in FILES_DIR//. Once the session is created, move them into the date-bucketed * uploads dir and record the new path so they're co-located with the session. */ export declare function rehomeAttachmentsToSession(fileIds: unknown, sessionId: string): void; /** Entry point for POST /api/sessions/:id/attachments (called from api.ts after session validation). */ export declare function handleSessionAttachment(req: HttpRequest, res: ServerResponse, sessionId: string, context: ApiContext): Promise; /** Strong ETag derived from the immutable id + byte size. */ export declare function fileEtag(id: string, size: number): string; /** * Decide whether a conditional GET can be answered with 304 Not Modified. * If-None-Match wins over If-Modified-Since (per RFC 7232). ETag comparison * tolerates weak prefixes and comma-separated lists; "*" always matches. */ export declare function isFileNotModified(headers: HttpRequest["headers"], etag: string, lastModifiedMs: number): boolean; /** Route handler for all /api/files endpoints. Returns true if handled. */ export declare function handleFilesRequest(req: HttpRequest, res: ServerResponse, route: ParsedRoute, context: ApiContext): Promise; //# sourceMappingURL=files.d.ts.map