* 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>
97 lines
3.1 KiB
TypeScript
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);
|
|
}
|