/** * Where the bytes of an inbound file land. * * The transport already turns a file message into a `` marker inside the message text, so the model sees the NAME * and never the content. That is worse than seeing nothing: handed a file name * and no bytes, a model guesses what the log said and answers as though it had * read it. This module makes the name true — every file a message carried is * streamed into the conversation's workspace, and a note riding the same text * says where it now is. * * The split with `images.ts` is deliberate: this module owns "how bytes reach * the disk", `images.ts` owns "how an image becomes a content block the model * can see". An image passes through both. The other direction — how bytes leave * a workspace for a chat — is `outbound-file.ts`, and shares nothing with this * but the containment primitive both ask the filesystem for. * * The per-file limit can only be judged AFTER the download. A * `ResourceDescriptor` carries no size and the transport offers no size probe, * so the verdict is `bytesWritten` and an over-limit file is unlinked the * moment it lands. The bytes really do touch the disk once; that is the * transport's surface, not a choice made here. * * Sanitization below buys filesystem safety and nothing else. A sanitized name * is still attacker-chosen text that rides into the model's prompt, and the * defense at that layer is the standing presence line, not this one. It also * cannot see a symlink, which is why the landing directory is canonicalized * before anything is written into it: the name is safe and the PATH still has * to be proven to be inside the workspace. * @module dsh-lark-channel/files */ import type { NormalizedMessage } from '@larksuite/channel'; /** * How many per-file budgets one message may spend. Deliberately not * configurable: its only job is to stop "twenty 20 MB files in one message", * and it moves with the per-file limit by construction — a knob of its own * would answer no question the per-file limit leaves open. */ export declare const MESSAGE_BYTES_FACTOR = 3; /** The inbound half of the transport, as this module uses it. */ export interface InboundFilePort { downloadResourceToFile(messageId: string, fileKey: string, type: 'image' | 'file', destPath: string): Promise<{ contentType?: string; bytesWritten: number; }>; } /** One inbound file that reached the workspace. */ export interface LandedFile { readonly fileKey: string; readonly type: 'file' | 'image' | 'audio' | 'video'; /** Absolute path on disk. */ readonly path: string; readonly bytes: number; readonly contentType?: string | undefined; /** The sanitized name it landed under. */ readonly fileName: string; } /** What one message's files became. */ export interface CollectedFiles { readonly landed: readonly LandedFile[]; /** One line per thing the model must know about, exactly as it rides the text. */ readonly notes: readonly string[]; } /** How {@link collectInboundFiles} is bounded and where it writes. */ export interface InboundOptions { /** The conversation's workspace, i.e. what `/cd` currently points at. */ readonly workspace: string; /** Whether this deployment accepts files at all. */ readonly enabled: boolean; readonly maxFileBytes: number; /** Operator console line. */ readonly report: (line: string) => void; /** * Whether this workspace has yet to be told where inbound files land; the * `.gitignore` hint rides the first landing rather than every message. */ readonly hintWorkspace?: boolean | undefined; } /** * Turn the sender's file name into one component safe to join onto a directory. * * The order is the substance. `basename` runs first because the input may be a * whole path; the separator pass cannot then be dropped, because POSIX * `basename` does not split on `\` and a Windows-shaped name reaches a Linux * host unchanged. Stripping leading dots is what turns `..` — a name that * escapes the directory — into nothing at all. * * Filesystem safety is all this buys. The name still rides into the note the * model reads; that layer's defense is the standing presence line. * @param name - the sender's file name, absent when the message carried none. * @returns a single path component, never empty. */ export declare function sanitizeFileName(name: string | undefined): string; /** * Land every file one message carried in the conversation's workspace. * * Nothing is dropped silently: a file that is refused, too big, or fails to * download leaves a note the model reads, because a model that received a file * and believes it did not is worse off than one that knows what it is missing. * @param msg - the inbound message. * @param port - transport used to stream the bytes to disk. * @param options - workspace, switch, and the per-file budget. * @returns the files on disk and the notes to append to the text. */ export declare function collectInboundFiles(msg: NormalizedMessage, port: InboundFilePort, options: InboundOptions): Promise; //# sourceMappingURL=files.d.ts.map