mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-17 13:05:32 +09:00
fix(api): support int53 attachment sizes (#1261)
This commit is contained in:
1 parent
40d1e90a3c
commit
3a8ed1012f
22 files changed
+289
-89
No files matched your search
@@ -36,6 +36,7 @@ import {
|
||||
createStringType,
|
||||
Int32Type,
|
||||
Int64StringType,
|
||||
NonNegativeSafeIntegerType,
|
||||
SnowflakeStringType,
|
||||
SnowflakeType,
|
||||
withOpenApiType,
|
||||
@@ -800,7 +801,7 @@ const LimitRuleSchema = z.object({
|
||||
id: z.string().min(1).describe('Unique rule identifier'),
|
||||
filters: LimitFilterSchema.optional().describe('Optional filters that scope the rule'),
|
||||
limits: z
|
||||
.record(z.string(), z.number().min(0))
|
||||
.record(z.string(), NonNegativeSafeIntegerType)
|
||||
.refine(
|
||||
(limits) => {
|
||||
const limitKeys = Object.keys(limits);
|
||||
@@ -1258,7 +1259,7 @@ const AdminMessageAttachmentSchema = z.object({
|
||||
content_type: z.string().nullable(),
|
||||
width: Int32Type.nullable(),
|
||||
height: Int32Type.nullable(),
|
||||
size: Int32Type.nullable().optional(),
|
||||
size: NonNegativeSafeIntegerType.nullable().optional(),
|
||||
ncmec_status: NcmecSubmissionStatusEnum,
|
||||
ncmec_report_id: createStringType(1, 256).nullable(),
|
||||
ncmec_failure_reason: createStringType(1, 4000).nullable(),
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {GifMediaFormat} from '@fluxer/schema/src/domains/gif/GifSchemas';
|
||||
import {createStringType, SnowflakeStringType, SnowflakeType} from '@fluxer/schema/src/primitives/SchemaPrimitives';
|
||||
import {
|
||||
createStringType,
|
||||
NonNegativeSafeIntegerType,
|
||||
SnowflakeStringType,
|
||||
SnowflakeType,
|
||||
} from '@fluxer/schema/src/primitives/SchemaPrimitives';
|
||||
import {z} from 'zod';
|
||||
|
||||
const FavoriteMemeBase = z.object({
|
||||
@@ -67,7 +72,7 @@ export const FavoriteMemeResponse = z.object({
|
||||
filename: z.string().describe('Original filename of the meme'),
|
||||
content_type: z.string().describe('MIME type of the meme file'),
|
||||
content_hash: z.string().nullish().describe('Hash of the file content for deduplication'),
|
||||
size: z.number().describe('File size in bytes'),
|
||||
size: NonNegativeSafeIntegerType.describe('File size in bytes'),
|
||||
width: z.number().int().nullish().describe('Width of the image or video in pixels'),
|
||||
height: z.number().int().nullish().describe('Height of the image or video in pixels'),
|
||||
duration: z.number().nullish().describe('Duration of the video in seconds'),
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
createBitflagInt32Type,
|
||||
createStringType,
|
||||
Int32Type,
|
||||
NonNegativeSafeIntegerType,
|
||||
SnowflakeType,
|
||||
} from '@fluxer/schema/src/primitives/SchemaPrimitives';
|
||||
import {z} from 'zod';
|
||||
@@ -41,7 +42,7 @@ export const ClientUploadedAttachmentRequest = ClientAttachmentBase.extend({
|
||||
upload_filename: createStringType(1, 4096).describe(
|
||||
'Temporary upload key returned by the attachment upload endpoint',
|
||||
),
|
||||
file_size: coerceNumberFromString(Int32Type).describe('Uploaded file size in bytes'),
|
||||
file_size: coerceNumberFromString(NonNegativeSafeIntegerType).describe('Uploaded file size in bytes'),
|
||||
content_type: createStringType(1, 255).describe('MIME type of the uploaded file'),
|
||||
});
|
||||
|
||||
|
||||
@@ -2,14 +2,19 @@
|
||||
|
||||
import {ATTACHMENT_UPLOAD_MAX_CHUNKS, MAX_ATTACHMENTS_PER_MESSAGE} from '@fluxer/constants/src/LimitConstants';
|
||||
import {FilenameType} from '@fluxer/schema/src/primitives/FileValidators';
|
||||
import {coerceNumberFromString, createStringType, Int32Type} from '@fluxer/schema/src/primitives/SchemaPrimitives';
|
||||
import {
|
||||
coerceNumberFromString,
|
||||
createStringType,
|
||||
Int32Type,
|
||||
NonNegativeSafeIntegerType,
|
||||
} from '@fluxer/schema/src/primitives/SchemaPrimitives';
|
||||
import {URLType} from '@fluxer/schema/src/primitives/UrlValidators';
|
||||
import {z} from 'zod';
|
||||
|
||||
export const PresignedAttachmentUploadRequestItem = z.object({
|
||||
id: coerceNumberFromString(Int32Type).describe('The client-side identifier for this attachment'),
|
||||
filename: FilenameType.describe('The name of the file that will be uploaded'),
|
||||
file_size: coerceNumberFromString(Int32Type).describe('Expected file size in bytes'),
|
||||
file_size: coerceNumberFromString(NonNegativeSafeIntegerType).describe('Expected file size in bytes'),
|
||||
content_type: createStringType(1, 255).describe('MIME type the client will upload'),
|
||||
});
|
||||
|
||||
@@ -29,7 +34,7 @@ const PresignedAttachmentUploadBase = z.object({
|
||||
id: coerceNumberFromString(Int32Type).describe('The client-side identifier for this attachment'),
|
||||
filename: FilenameType.describe('The original filename for this upload'),
|
||||
upload_filename: createStringType(1, 4096).describe('Temporary upload key to reference in message send payloads'),
|
||||
file_size: coerceNumberFromString(Int32Type).describe('Expected file size in bytes'),
|
||||
file_size: coerceNumberFromString(NonNegativeSafeIntegerType).describe('Expected file size in bytes'),
|
||||
content_type: createStringType(1, 255).describe('Expected MIME type for this upload'),
|
||||
});
|
||||
const PresignedAttachmentUploadSinglepart = PresignedAttachmentUploadBase.extend({
|
||||
@@ -45,7 +50,7 @@ const PresignedAttachmentUploadPart = z.object({
|
||||
const PresignedAttachmentUploadMultipart = PresignedAttachmentUploadBase.extend({
|
||||
upload_mode: z.literal('multipart'),
|
||||
upload_id: createStringType(1, 1024).describe('S3 multipart upload identifier; required to complete the upload'),
|
||||
part_size: coerceNumberFromString(Int32Type).describe('Size in bytes of each part except the last'),
|
||||
part_size: coerceNumberFromString(NonNegativeSafeIntegerType).describe('Size in bytes of each part except the last'),
|
||||
parts: z
|
||||
.array(PresignedAttachmentUploadPart)
|
||||
.min(1)
|
||||
|
||||
@@ -11,7 +11,12 @@ import type {GuildMemberData} from '@fluxer/schema/src/domains/guild/GuildMember
|
||||
import {type MessageEmbed, MessageEmbedResponse} from '@fluxer/schema/src/domains/message/EmbedSchemas';
|
||||
import {type UserPartial, UserPartialResponse} from '@fluxer/schema/src/domains/user/UserResponseSchemas';
|
||||
import {MessageReferenceTypeSchema, MessageTypeSchema} from '@fluxer/schema/src/primitives/MessageValidators';
|
||||
import {createBitflagInt32Type, Int32Type, SnowflakeStringType} from '@fluxer/schema/src/primitives/SchemaPrimitives';
|
||||
import {
|
||||
createBitflagInt32Type,
|
||||
Int32Type,
|
||||
NonNegativeSafeIntegerType,
|
||||
SnowflakeStringType,
|
||||
} from '@fluxer/schema/src/primitives/SchemaPrimitives';
|
||||
import {z} from 'zod';
|
||||
|
||||
export const MessageAttachmentResponse = z.object({
|
||||
@@ -21,7 +26,7 @@ export const MessageAttachmentResponse = z.object({
|
||||
description: z.string().nullish().describe('The description of the attachment'),
|
||||
content_type: z.string().nullish().describe('The MIME type of the attachment'),
|
||||
content_hash: z.string().nullish().describe('The hash of the attachment content'),
|
||||
size: Int32Type.describe('The size of the attachment in bytes'),
|
||||
size: NonNegativeSafeIntegerType.describe('The size of the attachment in bytes'),
|
||||
url: z.string().nullish().describe('The URL of the attachment'),
|
||||
proxy_url: z.string().nullish().describe('The proxied URL of the attachment'),
|
||||
width: Int32Type.nullish().describe('The width of the attachment in pixels (for images/videos)'),
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {
|
||||
PresignedAttachmentUploadRequest,
|
||||
PresignedAttachmentUploadResponse,
|
||||
} from '@fluxer/schema/src/domains/message/AttachmentUploadSchemas';
|
||||
import {ClientUploadedAttachmentRequest} from '@fluxer/schema/src/domains/message/AttachmentSchemas';
|
||||
import {MessageAttachmentResponse} from '@fluxer/schema/src/domains/message/MessageResponseSchemas';
|
||||
import {describe, expect, it} from 'vitest';
|
||||
|
||||
describe('attachment byte size schemas', () => {
|
||||
it('accepts upload request file sizes above int32', () => {
|
||||
const result = PresignedAttachmentUploadRequest.safeParse({
|
||||
attachments: [
|
||||
{
|
||||
id: 0,
|
||||
filename: 'large.bin',
|
||||
file_size: 2147483648,
|
||||
content_type: 'application/octet-stream',
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it('accepts upload response file and part sizes above int32', () => {
|
||||
const result = PresignedAttachmentUploadResponse.safeParse({
|
||||
attachments: [
|
||||
{
|
||||
id: 0,
|
||||
filename: 'large.bin',
|
||||
upload_filename: 'uploads/tmp-large',
|
||||
file_size: 2147483648,
|
||||
content_type: 'application/octet-stream',
|
||||
upload_mode: 'multipart',
|
||||
upload_id: 'multipart-id',
|
||||
part_size: 2147483648,
|
||||
parts: [{part_number: 1, upload_url: 'https://uploads.example.test/part'}],
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it('accepts completed upload file sizes up to the JavaScript safe integer limit', () => {
|
||||
const result = ClientUploadedAttachmentRequest.safeParse({
|
||||
id: 0,
|
||||
filename: 'max.bin',
|
||||
upload_filename: 'uploads/tmp-max',
|
||||
file_size: Number.MAX_SAFE_INTEGER,
|
||||
content_type: 'application/octet-stream',
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it('rejects unsafe upload file sizes', () => {
|
||||
const result = ClientUploadedAttachmentRequest.safeParse({
|
||||
id: 0,
|
||||
filename: 'unsafe.bin',
|
||||
upload_filename: 'uploads/tmp-unsafe',
|
||||
file_size: Number.MAX_SAFE_INTEGER + 1,
|
||||
content_type: 'application/octet-stream',
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it('emits message attachment sizes as safe integer numbers', () => {
|
||||
const result = MessageAttachmentResponse.safeParse({
|
||||
id: '123456789012345678',
|
||||
filename: 'large.bin',
|
||||
title: null,
|
||||
description: null,
|
||||
content_type: 'application/octet-stream',
|
||||
content_hash: null,
|
||||
size: Number.MAX_SAFE_INTEGER,
|
||||
url: 'https://cdn.example.test/large.bin',
|
||||
proxy_url: 'https://cdn.example.test/large.bin',
|
||||
width: null,
|
||||
height: null,
|
||||
placeholder: null,
|
||||
flags: 0,
|
||||
nsfw: null,
|
||||
duration: null,
|
||||
waveform: null,
|
||||
expires_at: null,
|
||||
expired: null,
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
createStringType,
|
||||
createUnboundedStringType,
|
||||
Int32Type,
|
||||
NonNegativeSafeIntegerType,
|
||||
SnowflakeType,
|
||||
} from '@fluxer/schema/src/primitives/SchemaPrimitives';
|
||||
import {URLType} from '@fluxer/schema/src/primitives/UrlValidators';
|
||||
@@ -68,7 +69,7 @@ const WebhookAttachmentRequest = z.object({
|
||||
filename: createStringType(1, 1024).optional().describe('Name of the file (1-1024 characters)'),
|
||||
description: createStringType(1, 4096).optional().describe('Description for the attachment (max 4096 characters)'),
|
||||
content_type: createStringType(1, 256).optional().describe('MIME type of the file'),
|
||||
size: z.number().int().optional().describe('Size of the file in bytes'),
|
||||
size: NonNegativeSafeIntegerType.optional().describe('Size of the file in bytes'),
|
||||
url: URLType.optional().describe('URL of the attachment'),
|
||||
proxy_url: URLType.optional().describe('Proxied URL of the attachment'),
|
||||
height: z.number().int().optional().describe('Height of the image/video in pixels'),
|
||||
|
||||
@@ -171,6 +171,12 @@ export const ColorType = z
|
||||
.describe('fluxer:ColorType');
|
||||
export const Int32Type = z.number().int().min(0).max(2147483647).describe('fluxer:Int32Type');
|
||||
export const SignedInt32Type = z.number().int().min(-2147483648).max(2147483647).describe('fluxer:SignedInt32Type');
|
||||
export const NonNegativeSafeIntegerType = z
|
||||
.number()
|
||||
.int()
|
||||
.min(0)
|
||||
.max(Number.MAX_SAFE_INTEGER)
|
||||
.describe('fluxer:NonNegativeSafeIntegerType');
|
||||
const INTEGER_STRING_REGEX = /^[+-]?\d+$/;
|
||||
|
||||
function coerceNumericStringToNumber(value: unknown): unknown {
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
Int32Type,
|
||||
Int64StringType,
|
||||
Int64Type,
|
||||
NonNegativeSafeIntegerType,
|
||||
normalizeString,
|
||||
normalizeWhitespace,
|
||||
removeStandaloneSurrogates,
|
||||
@@ -214,6 +215,29 @@ describe('Int32Type', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('NonNegativeSafeIntegerType', () => {
|
||||
it('accepts maximum JavaScript safe integer', () => {
|
||||
const result = NonNegativeSafeIntegerType.safeParse(Number.MAX_SAFE_INTEGER);
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
it('accepts values above int32', () => {
|
||||
const result = NonNegativeSafeIntegerType.safeParse(2147483648);
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
it('rejects unsafe JavaScript integers', () => {
|
||||
const result = NonNegativeSafeIntegerType.safeParse(Number.MAX_SAFE_INTEGER + 1);
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
it('rejects negative values', () => {
|
||||
const result = NonNegativeSafeIntegerType.safeParse(-1);
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
it('rejects non-integer values', () => {
|
||||
const result = NonNegativeSafeIntegerType.safeParse(1.5);
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('coerceNumberFromString', () => {
|
||||
it('coerces valid integer strings to numbers', () => {
|
||||
const schema = coerceNumberFromString(z.number().int().min(0).max(100));
|
||||
|
||||
Reference in new issue
Block a user