/**
* 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