evOo-local/packages/contracts/src/attachments.ts
cynarAI 797906dde2
Allow repository ZIP uploads up to 5 GiB (#194)
* Allow ZIP uploads up to 5 GiB and stream them onto Docker computers

Repository archives could not be picked at all: application/zip was not an
allowed attachment type. ZIPs are now allowed with their own 5 GiB cap,
upload as a stream, and reach the bot's Docker computer through a new
streamed supervisor write that commits only after the body ended cleanly.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tgvrj34NMnaqvtgvruYQZZ

* Use a disallowed type for the invalid-attachment check now that ZIP is allowed

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tgvrj34NMnaqvtgvruYQZZ

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-10 12:35:32 +02:00

97 lines
3.1 KiB
TypeScript

export const ATTACHMENT_MAX_BYTES = 10 * 1024 * 1024;
export const ATTACHMENT_MAX_COUNT = 4;
/** Base64 expands payload by 4/3; cap before decode to reject oversize uploads cheaply. */
export const ATTACHMENT_MAX_BASE64_LENGTH = Math.ceil(ATTACHMENT_MAX_BYTES / 3) * 4;
export const ATTACHMENT_IMAGE_MIME_TYPES = [
"image/jpeg",
"image/png",
"image/webp",
"image/gif",
] as const;
export const ATTACHMENT_VIDEO_MIME_TYPES = ["video/mp4", "video/webm", "video/quicktime"] as const;
export const ATTACHMENT_AUDIO_MIME_TYPES = [
"audio/mpeg",
"audio/mp4",
"audio/wav",
"audio/webm",
"audio/ogg",
] as const;
export const ATTACHMENT_FILE_MIME_TYPES = [
"application/pdf",
"text/plain",
"text/markdown",
"text/csv",
"application/json",
"text/vtt",
"application/x-subrip",
"application/zip",
] as const;
export const ATTACHMENT_ALLOWED_MIME_TYPES = [
...ATTACHMENT_IMAGE_MIME_TYPES,
...ATTACHMENT_VIDEO_MIME_TYPES,
...ATTACHMENT_AUDIO_MIME_TYPES,
...ATTACHMENT_FILE_MIME_TYPES,
] as const;
export type AttachmentKind = "image" | "video" | "audio" | "file";
/**
* Size caps for artifacts that do not travel inline as base64 (streamed uploads, generated and
* edited media). Inline RPC uploads stay capped by ATTACHMENT_MAX_BYTES.
*/
export const ARTIFACT_MAX_BYTES_BY_KIND: Record<AttachmentKind, number> = {
image: 25 * 1024 * 1024,
video: 500 * 1024 * 1024,
audio: 200 * 1024 * 1024,
file: 25 * 1024 * 1024,
};
/** A ZIP carries a whole project or repository, so it gets a far larger cap than other files. */
export const ARCHIVE_MAX_BYTES = 5 * 1024 * 1024 * 1024;
/** Largest artifact of this MIME type that may be streamed or generated. */
export function artifactMaxBytes(mimeType: string): number {
return mimeType === "application/zip"
? ARCHIVE_MAX_BYTES
: ARTIFACT_MAX_BYTES_BY_KIND[attachmentKindOf(mimeType)];
}
export type AttachmentMimeType = (typeof ATTACHMENT_ALLOWED_MIME_TYPES)[number];
export function isAttachmentImageMimeType(mimeType: string): boolean {
return (ATTACHMENT_IMAGE_MIME_TYPES as readonly string[]).includes(mimeType);
}
export function isAttachmentVideoMimeType(mimeType: string): boolean {
return (ATTACHMENT_VIDEO_MIME_TYPES as readonly string[]).includes(mimeType);
}
export function isAttachmentAudioMimeType(mimeType: string): boolean {
return (ATTACHMENT_AUDIO_MIME_TYPES as readonly string[]).includes(mimeType);
}
export function attachmentKindOf(mimeType: string): AttachmentKind {
if (isAttachmentImageMimeType(mimeType)) return "image";
if (isAttachmentVideoMimeType(mimeType)) return "video";
if (isAttachmentAudioMimeType(mimeType)) return "audio";
return "file";
}
export function isAllowedAttachmentMimeType(mimeType: string): mimeType is AttachmentMimeType {
return (ATTACHMENT_ALLOWED_MIME_TYPES as readonly string[]).includes(mimeType);
}
export function validateThreadsSendInput(input: {
text?: string;
artifactIds?: string[];
}): boolean {
const text = input.text?.trim() ?? "";
const artifactIds = input.artifactIds ?? [];
return Boolean(text || artifactIds.length);
}