fix(api): support int53 attachment sizes (#1261)

This commit is contained in:
Hampus
2026-07-02 23:41:55 +02:00
committed by GitHub
parent 40d1e90a3c
commit 3a8ed1012f
22 changed files with 289 additions and 89 deletions
+41 -37
View File
@@ -8977,7 +8977,7 @@
"type": "integer",
"minimum": 0,
"maximum": 8640000000000000,
"format": "int64",
"format": "int53",
"description": "Inclusive start timestamp in milliseconds"
}
},
@@ -8989,7 +8989,7 @@
"type": "integer",
"minimum": 0,
"maximum": 8640000000000000,
"format": "int64",
"format": "int53",
"description": "Inclusive end timestamp in milliseconds"
}
},
@@ -9087,7 +9087,7 @@
"type": "integer",
"minimum": 0,
"maximum": 8640000000000000,
"format": "int64",
"format": "int53",
"description": "Inclusive start timestamp in milliseconds"
}
},
@@ -9099,7 +9099,7 @@
"type": "integer",
"minimum": 0,
"maximum": 8640000000000000,
"format": "int64",
"format": "int53",
"description": "Inclusive end timestamp in milliseconds"
}
},
@@ -10613,7 +10613,7 @@
"type": "integer",
"minimum": 0,
"maximum": 9007199254740991,
"format": "int64",
"format": "int53",
"description": "Number of entries to skip"
}
}
@@ -10645,7 +10645,7 @@
"type": "integer",
"minimum": 0,
"maximum": 9007199254740991,
"format": "int64",
"format": "int53",
"description": "Number of entries to skip"
}
}
@@ -11147,7 +11147,7 @@
"amount_cents": {
"type": "integer",
"maximum": 9007199254740991,
"format": "int64",
"format": "int53",
"minimum": 0,
"exclusiveMinimum": true
},
@@ -11981,7 +11981,7 @@
"properties": {
"guild_id": {"$ref": "#/components/schemas/SnowflakeType"},
"limit": {"type": "integer", "minimum": 1, "maximum": 200, "format": "int32"},
"offset": {"type": "integer", "minimum": 0, "maximum": 9007199254740991, "format": "int64"}
"offset": {"type": "integer", "minimum": 0, "maximum": 9007199254740991, "format": "int53"}
},
"required": ["guild_id"]
},
@@ -12219,7 +12219,7 @@
"properties": {
"query": {"type": "string"},
"limit": {"type": "integer", "minimum": 1, "maximum": 200, "format": "int32"},
"offset": {"type": "integer", "minimum": 0, "maximum": 9007199254740991, "format": "int64"}
"offset": {"type": "integer", "minimum": 0, "maximum": 9007199254740991, "format": "int53"}
}
},
"ShutdownGuildRequest": {
@@ -12439,9 +12439,9 @@
"type": "integer",
"minimum": 1,
"maximum": 9007199254740991,
"format": "int64"
"format": "int53"
},
"use_count": {"type": "integer", "minimum": 0, "maximum": 9007199254740991, "format": "int64"},
"use_count": {"type": "integer", "minimum": 0, "maximum": 9007199254740991, "format": "int53"},
"revoked_at": {"nullable": true, "type": "string", "format": "date-time"},
"approval_required": {"type": "boolean"},
"last_used_at": {"nullable": true, "type": "string", "format": "date-time"},
@@ -12668,7 +12668,7 @@
"logo_uri": {"nullable": true, "type": "string"},
"tos_uri": {"nullable": true, "type": "string"},
"policy_uri": {"nullable": true, "type": "string"},
"key_count": {"type": "integer", "minimum": 0, "maximum": 9007199254740991, "format": "int64"}
"key_count": {"type": "integer", "minimum": 0, "maximum": 9007199254740991, "format": "int53"}
},
"required": [
"enabled",
@@ -12698,7 +12698,7 @@
"nullable": true,
"type": "integer",
"maximum": 9007199254740991,
"format": "int64",
"format": "int53",
"minimum": 0,
"exclusiveMinimum": true
},
@@ -12706,7 +12706,7 @@
"nullable": true,
"type": "integer",
"maximum": 9007199254740991,
"format": "int64",
"format": "int53",
"minimum": 0,
"exclusiveMinimum": true
},
@@ -12715,7 +12715,7 @@
"nullable": true,
"type": "integer",
"maximum": 9007199254740991,
"format": "int64",
"format": "int53",
"minimum": 0,
"exclusiveMinimum": true
},
@@ -12723,7 +12723,7 @@
"nullable": true,
"type": "integer",
"maximum": 9007199254740991,
"format": "int64",
"format": "int53",
"minimum": 0,
"exclusiveMinimum": true
},
@@ -12737,14 +12737,14 @@
"min_lifetime_days": {
"type": "integer",
"maximum": 9007199254740991,
"format": "int64",
"format": "int53",
"minimum": 0,
"exclusiveMinimum": true
},
"max_lifetime_days": {
"type": "integer",
"maximum": 9007199254740991,
"format": "int64",
"format": "int53",
"minimum": 0,
"exclusiveMinimum": true
},
@@ -12752,14 +12752,14 @@
"renew_threshold_days": {
"type": "integer",
"maximum": 9007199254740991,
"format": "int64",
"format": "int53",
"minimum": 0,
"exclusiveMinimum": true
},
"renew_window_days": {
"type": "integer",
"maximum": 9007199254740991,
"format": "int64",
"format": "int53",
"minimum": 0,
"exclusiveMinimum": true
}
@@ -12871,9 +12871,9 @@
"type": "integer",
"minimum": 1,
"maximum": 9007199254740991,
"format": "int64"
"format": "int53"
},
"use_count": {"type": "integer", "minimum": 0, "maximum": 9007199254740991, "format": "int64"},
"use_count": {"type": "integer", "minimum": 0, "maximum": 9007199254740991, "format": "int53"},
"revoked_at": {"nullable": true, "type": "string", "format": "date-time"},
"approval_required": {"type": "boolean"},
"last_used_at": {"nullable": true, "type": "string", "format": "date-time"},
@@ -13064,7 +13064,7 @@
"nullable": true,
"type": "integer",
"maximum": 9007199254740991,
"format": "int64",
"format": "int53",
"minimum": 0,
"exclusiveMinimum": true
},
@@ -13072,7 +13072,7 @@
"nullable": true,
"type": "integer",
"maximum": 9007199254740991,
"format": "int64",
"format": "int53",
"minimum": 0,
"exclusiveMinimum": true
},
@@ -13081,7 +13081,7 @@
"nullable": true,
"type": "integer",
"maximum": 9007199254740991,
"format": "int64",
"format": "int53",
"minimum": 0,
"exclusiveMinimum": true
},
@@ -13089,7 +13089,7 @@
"nullable": true,
"type": "integer",
"maximum": 9007199254740991,
"format": "int64",
"format": "int53",
"minimum": 0,
"exclusiveMinimum": true
}
@@ -13296,7 +13296,7 @@
},
"limits": {
"type": "object",
"additionalProperties": {"type": "number", "minimum": 0},
"additionalProperties": {"$ref": "#/components/schemas/NonNegativeSafeIntegerType"},
"description": "Per-limit key values"
},
"modifiedFields": {"type": "array", "items": {"type": "string"}}
@@ -13352,6 +13352,7 @@
"limit_keys"
]
},
"NonNegativeSafeIntegerType": {"type": "integer", "minimum": 0, "maximum": 9007199254740991, "format": "int53"},
"LimitConfigUpdateRequest": {
"type": "object",
"properties": {
@@ -13387,7 +13388,7 @@
},
"limits": {
"type": "object",
"additionalProperties": {"type": "number", "minimum": 0},
"additionalProperties": {"$ref": "#/components/schemas/NonNegativeSafeIntegerType"},
"description": "Per-limit key values"
}
},
@@ -13455,7 +13456,7 @@
"content_type": {"nullable": true, "type": "string"},
"width": {"nullable": true, "allOf": [{"$ref": "#/components/schemas/Int32Type"}]},
"height": {"nullable": true, "allOf": [{"$ref": "#/components/schemas/Int32Type"}]},
"size": {"nullable": true, "allOf": [{"$ref": "#/components/schemas/Int32Type"}]},
"size": {"nullable": true, "allOf": [{"$ref": "#/components/schemas/NonNegativeSafeIntegerType"}]},
"ncmec_status": {
"type": "string",
"enum": ["not_submitted", "submitted", "failed"],
@@ -14124,8 +14125,8 @@
"size": {
"type": "integer",
"minimum": 0,
"maximum": 2147483647,
"format": "int32",
"maximum": 9007199254740991,
"format": "int53",
"description": "The size of the attachment in bytes"
},
"url": {"description": "The URL of the attachment", "nullable": true, "type": "string"},
@@ -14387,7 +14388,7 @@
"items": {"$ref": "#/components/schemas/MessageResponseSchema"},
"maxItems": 100
},
"total": {"type": "integer", "minimum": 0, "maximum": 9007199254740991, "format": "int64"}
"total": {"type": "integer", "minimum": 0, "maximum": 9007199254740991, "format": "int53"}
},
"required": ["messages", "total"]
},
@@ -14405,7 +14406,7 @@
"properties": {
"success": {"type": "boolean", "enum": [true]},
"job_id": {"type": "string"},
"requested": {"type": "integer", "minimum": 0, "maximum": 9007199254740991, "format": "int64"}
"requested": {"type": "integer", "minimum": 0, "maximum": 9007199254740991, "format": "int53"}
},
"required": ["success", "job_id"]
},
@@ -14558,7 +14559,10 @@
"content_type": {"nullable": true, "type": "string"},
"width": {"nullable": true, "allOf": [{"$ref": "#/components/schemas/Int32Type"}]},
"height": {"nullable": true, "allOf": [{"$ref": "#/components/schemas/Int32Type"}]},
"size": {"nullable": true, "allOf": [{"$ref": "#/components/schemas/Int32Type"}]},
"size": {
"nullable": true,
"allOf": [{"$ref": "#/components/schemas/NonNegativeSafeIntegerType"}]
},
"ncmec_status": {
"type": "string",
"enum": ["not_submitted", "submitted", "failed"],
@@ -14664,7 +14668,7 @@
"type": "integer",
"minimum": 0,
"maximum": 9007199254740991,
"format": "int64",
"format": "int53",
"description": "Number of reports to skip"
}
}
@@ -14712,7 +14716,7 @@
"type": "integer",
"minimum": 0,
"maximum": 9007199254740991,
"format": "int64",
"format": "int53",
"description": "Number of entries to skip"
},
"reporter_id": {"$ref": "#/components/schemas/SnowflakeType"},
@@ -15435,7 +15439,7 @@
"email": {"type": "string"},
"last_active_ip": {"type": "string"},
"limit": {"type": "integer", "minimum": 1, "maximum": 200, "format": "int32"},
"offset": {"type": "integer", "minimum": 0, "maximum": 9007199254740991, "format": "int64"}
"offset": {"type": "integer", "minimum": 0, "maximum": 9007199254740991, "format": "int53"}
}
},
"SendPasswordResetRequest": {
@@ -35,6 +35,7 @@ import {getPurgeQueue, getStorageService} from '../../middleware/ServiceSingleto
import {getMessageSearchService} from '../../SearchFactory';
import {deleteMessageSearchDocuments} from '../../search/MessageSearchIndexCleanup';
import {searchExistingMessages} from '../../search/MessageSearchResultReconciler';
import {assertSafeByteSize} from '../../utils/ByteSizeUtils';
import type {AdminAuditService} from './AdminAuditService';
interface AdminMessageServiceDeps {
@@ -330,7 +331,7 @@ export class AdminMessageService {
content_type: attachment.content_type ?? null,
width: attachment.width ?? null,
height: attachment.height ?? null,
size: attachment.size ?? null,
size: attachment.size == null ? null : assertSafeByteSize(attachment.size, 'admin message attachment size'),
ncmec_status: attachmentStatuses.get(attachment.id)?.status ?? 'not_submitted',
ncmec_report_id: attachmentStatuses.get(attachment.id)?.ncmec_report_id ?? null,
ncmec_failure_reason: attachmentStatuses.get(attachment.id)?.failure_reason ?? null,
@@ -38,6 +38,7 @@ import type {IARMessageContext, IARSubmission} from '../../report/IReportReposit
import type {ReportService} from '../../report/ReportService';
import {getReportSearchService} from '../../SearchFactory';
import type {UserChannelService} from '../../user/services/UserChannelService';
import {assertSafeByteSize} from '../../utils/ByteSizeUtils';
import type {AdminAuditService} from './AdminAuditService';
interface AdminReportServiceDeps {
@@ -580,7 +581,7 @@ export class AdminReportService {
content_type: attachment.content_type ?? null,
width: attachment.width ?? null,
height: attachment.height ?? null,
size: attachment.size != null ? Number(attachment.size) : null,
size: attachment.size != null ? assertSafeByteSize(attachment.size, 'admin report attachment size') : null,
ncmec_status: attachmentStatusesById.get(attachment.attachment_id.toString())?.status ?? 'not_submitted',
ncmec_report_id: attachmentStatusesById.get(attachment.attachment_id.toString())?.ncmec_report_id ?? null,
ncmec_failure_reason: attachmentStatusesById.get(attachment.attachment_id.toString())?.failure_reason ?? null,
@@ -4,6 +4,7 @@ import type {FavoriteMemeResponse} from '@fluxer/schema/src/domains/meme/MemeSch
import {userIdToChannelId} from '../BrandedTypes';
import {makeAttachmentCdnUrl} from '../channel/services/message/MessageHelpers';
import type {FavoriteMeme} from '../models/FavoriteMeme';
import {assertSafeByteSize} from '../utils/ByteSizeUtils';
export function mapFavoriteMemeToResponse(meme: FavoriteMeme): FavoriteMemeResponse {
const url = makeAttachmentCdnUrl(userIdToChannelId(meme.userId), meme.attachmentId, meme.filename);
@@ -17,7 +18,7 @@ export function mapFavoriteMemeToResponse(meme: FavoriteMeme): FavoriteMemeRespo
filename: meme.filename,
content_type: meme.contentType,
content_hash: meme.contentHash ?? null,
size: Number(meme.size),
size: assertSafeByteSize(meme.size, 'favorite meme size'),
width: meme.width ?? null,
height: meme.height ?? null,
duration: meme.duration ?? null,
+41 -34
View File
@@ -6224,7 +6224,7 @@
"type": "integer",
"minimum": 0,
"maximum": 9007199254740991,
"format": "int64",
"format": "int53",
"description": "Pagination offset"
}
}
@@ -27115,8 +27115,8 @@
"size": {
"type": "integer",
"minimum": 0,
"maximum": 2147483647,
"format": "int32",
"maximum": 9007199254740991,
"format": "int53",
"description": "The size of the attachment in bytes"
},
"url": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "The URL of the attachment"},
@@ -27900,7 +27900,7 @@
"type": "string",
"description": "Temporary upload key to reference in message send payloads"
},
"file_size": {"$ref": "#/components/schemas/Int32Type"},
"file_size": {"$ref": "#/components/schemas/NonNegativeSafeIntegerType"},
"content_type": {"type": "string", "description": "Expected MIME type for this upload"},
"upload_mode": {"type": "string", "enum": ["singlepart"]},
"upload_url": {
@@ -27910,6 +27910,7 @@
},
"required": ["id", "filename", "upload_filename", "file_size", "content_type", "upload_mode", "upload_url"]
},
"NonNegativeSafeIntegerType": {"type": "integer", "minimum": 0, "maximum": 9007199254740991, "format": "int53"},
"MultipartPresignedAttachmentUploadResponseItem": {
"type": "object",
"properties": {
@@ -27919,14 +27920,14 @@
"type": "string",
"description": "Temporary upload key to reference in message send payloads"
},
"file_size": {"$ref": "#/components/schemas/Int32Type"},
"file_size": {"$ref": "#/components/schemas/NonNegativeSafeIntegerType"},
"content_type": {"type": "string", "description": "Expected MIME type for this upload"},
"upload_mode": {"type": "string", "enum": ["multipart"]},
"upload_id": {
"type": "string",
"description": "S3 multipart upload identifier; required to complete the upload"
},
"part_size": {"$ref": "#/components/schemas/Int32Type"},
"part_size": {"$ref": "#/components/schemas/NonNegativeSafeIntegerType"},
"parts": {
"type": "array",
"items": {
@@ -27972,7 +27973,7 @@
"properties": {
"id": {"$ref": "#/components/schemas/Int32Type"},
"filename": {"type": "string", "description": "The name of the file that will be uploaded"},
"file_size": {"$ref": "#/components/schemas/Int32Type"},
"file_size": {"$ref": "#/components/schemas/NonNegativeSafeIntegerType"},
"content_type": {"type": "string", "description": "MIME type the client will upload"}
},
"required": ["id", "filename", "file_size", "content_type"]
@@ -28482,7 +28483,7 @@
"type": "string",
"pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(?:\\.\\d+)?(?:Z|[+-]\\d{2}:?\\d{2})?$"
},
{"type": "integer", "minimum": 0, "maximum": 8640000000000000, "format": "int64"},
{"type": "integer", "minimum": 0, "maximum": 8640000000000000, "format": "int53"},
{"type": "null"}
],
"description": "ISO8601 timestamp for the embed"
@@ -28599,7 +28600,7 @@
"type": "string",
"description": "Temporary upload key returned by the attachment upload endpoint"
},
"file_size": {"$ref": "#/components/schemas/Int32Type"},
"file_size": {"$ref": "#/components/schemas/NonNegativeSafeIntegerType"},
"content_type": {"type": "string", "description": "MIME type of the uploaded file"}
},
"required": ["id", "filename", "upload_filename", "file_size", "content_type"]
@@ -29234,7 +29235,13 @@
"anyOf": [{"type": "string"}, {"type": "null"}],
"description": "Hash of the file content for deduplication"
},
"size": {"type": "number", "description": "File size in bytes"},
"size": {
"type": "integer",
"minimum": 0,
"maximum": 9007199254740991,
"format": "int53",
"description": "File size in bytes"
},
"width": {
"anyOf": [{"type": "integer", "format": "int53"}, {"type": "null"}],
"description": "Width of the image or video in pixels"
@@ -29311,7 +29318,7 @@
},
"embed_index": {
"anyOf": [
{"type": "integer", "minimum": 0, "maximum": 9007199254740991, "format": "int64"},
{"type": "integer", "minimum": 0, "maximum": 9007199254740991, "format": "int53"},
{"type": "null"}
],
"description": "Index of the message embed to save as a meme"
@@ -29529,14 +29536,14 @@
},
"started_at_ms": {
"anyOf": [
{"type": "integer", "minimum": 0, "maximum": 9007199254740991, "format": "int64"},
{"type": "integer", "minimum": 0, "maximum": 9007199254740991, "format": "int53"},
{"type": "null"}
],
"description": "Session start Unix timestamp in milliseconds"
},
"expires_at_ms": {
"anyOf": [
{"type": "integer", "minimum": 0, "maximum": 9007199254740991, "format": "int64"},
{"type": "integer", "minimum": 0, "maximum": 9007199254740991, "format": "int53"},
{"type": "null"}
],
"description": "Session expiration Unix timestamp in milliseconds"
@@ -29605,7 +29612,7 @@
"type": "integer",
"minimum": 0,
"maximum": 9007199254740991,
"format": "int64",
"format": "int53",
"description": "Unix timestamp in milliseconds when this heartbeat expires"
}
},
@@ -30183,7 +30190,7 @@
"explicit_content_filter": {"type": "number", "description": "The explicit content filter level"},
"system_channel_id": {
"oneOf": [
{"type": "integer", "minimum": 0, "maximum": 9007199254740991, "format": "int64"},
{"type": "integer", "minimum": 0, "maximum": 9007199254740991, "format": "int53"},
{"type": "string"},
{"type": "null"}
],
@@ -30209,7 +30216,7 @@
"properties": {
"id": {
"oneOf": [
{"type": "integer", "minimum": 0, "maximum": 9007199254740991, "format": "int64"},
{"type": "integer", "minimum": 0, "maximum": 9007199254740991, "format": "int53"},
{"type": "string"}
],
"description": "The template-local role ID"
@@ -30218,14 +30225,14 @@
"permissions": {
"oneOf": [
{"type": "string"},
{"type": "integer", "minimum": 0, "maximum": 9007199254740991, "format": "int64"}
{"type": "integer", "minimum": 0, "maximum": 9007199254740991, "format": "int53"}
],
"description": "The permissions bitfield as a string (legacy)"
},
"permissions_new": {
"oneOf": [
{"type": "string"},
{"type": "integer", "minimum": 0, "maximum": 9007199254740991, "format": "int64"}
{"type": "integer", "minimum": 0, "maximum": 9007199254740991, "format": "int53"}
],
"description": "The permissions bitfield as a string (preferred)"
},
@@ -30244,7 +30251,7 @@
"properties": {
"id": {
"oneOf": [
{"type": "integer", "minimum": 0, "maximum": 9007199254740991, "format": "int64"},
{"type": "integer", "minimum": 0, "maximum": 9007199254740991, "format": "int53"},
{"type": "string"}
],
"description": "The template-local channel ID"
@@ -30255,7 +30262,7 @@
"position": {"type": "number", "description": "The position of the channel"},
"parent_id": {
"oneOf": [
{"type": "integer", "minimum": 0, "maximum": 9007199254740991, "format": "int64"},
{"type": "integer", "minimum": 0, "maximum": 9007199254740991, "format": "int53"},
{"type": "string"},
{"type": "null"}
],
@@ -30279,7 +30286,7 @@
"properties": {
"id": {
"oneOf": [
{"type": "integer", "minimum": 0, "maximum": 9007199254740991, "format": "int64"},
{"type": "integer", "minimum": 0, "maximum": 9007199254740991, "format": "int53"},
{"type": "string"}
],
"description": "The ID of the role or user for this overwrite"
@@ -30291,14 +30298,14 @@
"allow": {
"oneOf": [
{"type": "string"},
{"type": "integer", "minimum": 0, "maximum": 9007199254740991, "format": "int64"}
{"type": "integer", "minimum": 0, "maximum": 9007199254740991, "format": "int53"}
],
"description": "The allowed permissions bitfield as a string"
},
"deny": {
"oneOf": [
{"type": "string"},
{"type": "integer", "minimum": 0, "maximum": 9007199254740991, "format": "int64"}
{"type": "integer", "minimum": 0, "maximum": 9007199254740991, "format": "int53"}
],
"description": "The denied permissions bitfield as a string"
}
@@ -30960,7 +30967,7 @@
"type": "integer",
"minimum": 0,
"maximum": 9007199254740991,
"format": "int64",
"format": "int53",
"description": "New position for the channel"
},
"parent_id": {
@@ -31336,7 +31343,7 @@
"type": "integer",
"minimum": 0,
"maximum": 9007199254740991,
"format": "int64",
"format": "int53",
"description": "Number of results to skip for pagination"
},
"role_ids": {
@@ -33959,7 +33966,7 @@
"type": "integer",
"minimum": 1,
"maximum": 9007199254740991,
"format": "int64",
"format": "int53",
"description": "Page number for pagination (ignored when cursor is provided)"
},
"cursor": {
@@ -35434,7 +35441,7 @@
"type": "string",
"pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(?:\\.\\d+)?(?:Z|[+-]\\d{2}:?\\d{2})?$"
},
{"type": "integer", "minimum": 0, "maximum": 8640000000000000, "format": "int64"},
{"type": "integer", "minimum": 0, "maximum": 8640000000000000, "format": "int53"},
{"type": "null"}
],
"description": "When the mute expires"
@@ -35479,7 +35486,7 @@
"type": "string",
"pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(?:\\.\\d+)?(?:Z|[+-]\\d{2}:?\\d{2})?$"
},
{"type": "integer", "minimum": 0, "maximum": 8640000000000000, "format": "int64"},
{"type": "integer", "minimum": 0, "maximum": 8640000000000000, "format": "int53"},
{"type": "null"}
],
"description": "When the mute expires"
@@ -35530,7 +35537,7 @@
"type": "string",
"pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(?:\\.\\d+)?(?:Z|[+-]\\d{2}:?\\d{2})?$"
},
{"type": "integer", "minimum": 0, "maximum": 8640000000000000, "format": "int64"},
{"type": "integer", "minimum": 0, "maximum": 8640000000000000, "format": "int53"},
{"type": "null"}
],
"description": "When the mute expires"
@@ -36416,7 +36423,7 @@
"type": "integer",
"maximum": 9007199254740991,
"exclusiveMinimum": 0,
"format": "int64"
"format": "int53"
}
}
},
@@ -36977,7 +36984,7 @@
"type": "string",
"pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(?:\\.\\d+)?(?:Z|[+-]\\d{2}:?\\d{2})?$"
},
{"type": "integer", "minimum": 0, "maximum": 8640000000000000, "format": "int64"},
{"type": "integer", "minimum": 0, "maximum": 8640000000000000, "format": "int53"},
{"type": "null"}
],
"description": "When status resets"
@@ -37157,7 +37164,7 @@
"type": "string",
"pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(?:\\.\\d+)?(?:Z|[+-]\\d{2}:?\\d{2})?$"
},
{"type": "integer", "minimum": 0, "maximum": 8640000000000000, "format": "int64"},
{"type": "integer", "minimum": 0, "maximum": 8640000000000000, "format": "int53"},
{"type": "null"}
],
"description": "When the custom status expires"
@@ -37423,7 +37430,7 @@
"description": "Description for the attachment (max 4096 characters)"
},
"content_type": {"type": "string", "description": "MIME type of the file"},
"size": {"type": "integer", "format": "int53", "description": "Size of the file in bytes"},
"size": {"$ref": "#/components/schemas/NonNegativeSafeIntegerType"},
"url": {"type": "string", "description": "URL of the attachment"},
"proxy_url": {"type": "string", "description": "Proxied URL of the attachment"},
"height": {"type": "integer", "format": "int53", "description": "Height of the image/video in pixels"},
@@ -37896,7 +37903,7 @@
"type": "integer",
"minimum": 0,
"maximum": 9007199254740991,
"format": "int64",
"format": "int53",
"description": "Unix timestamp for the attachment footer"
},
"author_name": {"type": "string", "description": "Name of the author"},
@@ -42,6 +42,7 @@ import type {Message} from '../../models/Message';
import type {MessageSnapshot} from '../../models/MessageSnapshot';
import type {StickerItem} from '../../models/StickerItem';
import {mapUserToPartialResponse} from '../../user/UserMappers';
import {assertSafeByteSize} from '../../utils/ByteSizeUtils';
class NoopNatsConnectionManager implements INatsConnectionManager {
async connect(): Promise<void> {}
@@ -373,7 +374,7 @@ export class RepositoryBackedMessageResponseDataService extends MessageResponseD
description: attachment.description,
content_type: attachment.contentType,
content_hash: attachment.contentHash,
size: Number(attachment.size),
size: assertSafeByteSize(attachment.size, 'message attachment size'),
url,
proxy_url: url,
width: attachment.width,
+16
View File
@@ -0,0 +1,16 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
const MAX_SAFE_INTEGER_BIGINT = BigInt(Number.MAX_SAFE_INTEGER);
export function assertSafeByteSize(value: bigint | number, fieldName = 'byte size'): number {
if (typeof value === 'bigint') {
if (value < 0n || value > MAX_SAFE_INTEGER_BIGINT) {
throw new Error(`${fieldName} must fit a non-negative JavaScript safe integer`);
}
return Number(value);
}
if (!Number.isSafeInteger(value) || value < 0) {
throw new Error(`${fieldName} must fit a non-negative JavaScript safe integer`);
}
return value;
}
+10 -1
View File
@@ -58,6 +58,15 @@ const ENRICHMENT_QUERY_CONCURRENCY: usize = 16;
const REACTION_MESSAGE_BATCH_SIZE: usize = 64;
const ATTACHMENT_DECAY_BATCH_SIZE: usize = 128;
const BUCKET_INDEX_PAGE_SIZE: u32 = 200;
const JS_MAX_SAFE_INTEGER: i64 = 9_007_199_254_740_991;
fn assert_safe_byte_size(value: i64) -> i64 {
assert!(
(0..=JS_MAX_SAFE_INTEGER).contains(&value),
"attachment size must fit a non-negative JavaScript safe integer"
);
value
}
#[cfg(feature = "scylla")]
const MESSAGE_COLUMNS: &str = "\
@@ -1232,7 +1241,7 @@ impl MessagesShard {
description: attachment.description.clone(),
content_type: Some(content_type),
content_hash: attachment.content_hash.clone(),
size: attachment.size.unwrap_or_default(),
size: assert_safe_byte_size(attachment.size.unwrap_or_default()),
url: (!expired).then_some(url.clone()),
proxy_url: (!expired).then_some(url),
width: (!is_audio).then_some(attachment.width).flatten(),
@@ -17,6 +17,13 @@ export const Int32TypeSchema: OpenAPISchema = {
format: 'int32',
};
export const Int32TypeRef: OpenAPIRef = {$ref: '#/components/schemas/Int32Type'};
export const NonNegativeSafeIntegerTypeSchema: OpenAPISchema = {
type: 'integer',
minimum: 0,
maximum: 9007199254740991,
format: 'int53',
};
export const NonNegativeSafeIntegerTypeRef: OpenAPIRef = {$ref: '#/components/schemas/NonNegativeSafeIntegerType'};
export const Int64TypeSchema: OpenAPISchema = {
type: 'string',
format: 'int64',
@@ -371,7 +371,13 @@ export function zodToOpenAPISchema(schema: ZodTypeAny, depth = 0): OpenAPISchema
const min = result.minimum ?? result.exclusiveMinimum;
const max = result.maximum ?? result.exclusiveMaximum;
if (min != null && max != null) {
result.format = min >= -2147483648 && max <= 2147483647 ? 'int32' : 'int64';
if (min >= -2147483648 && max <= 2147483647) {
result.format = 'int32';
} else if (min >= Number.MIN_SAFE_INTEGER && max <= Number.MAX_SAFE_INTEGER) {
result.format = 'int53';
} else {
result.format = 'int64';
}
} else if (min == null && max == null) {
result.format = 'int53';
} else {
@@ -7,6 +7,7 @@ import {
Int64StringTypeRef,
Int64TypeRef,
LocaleRef,
NonNegativeSafeIntegerTypeRef,
PasswordTypeRef,
PhoneNumberTypeRef,
SnowflakeTypeRef,
@@ -51,6 +52,8 @@ function getRefForCustomTypeName(typeName: string): OpenAPISchemaOrRef | null {
return SnowflakeTypeRef;
case 'Int32Type':
return Int32TypeRef;
case 'NonNegativeSafeIntegerType':
return NonNegativeSafeIntegerTypeRef;
case 'Int64Type':
return Int64TypeRef;
case 'Int64StringType':
@@ -217,6 +220,7 @@ const FLUXER_CUSTOM_TYPES: Record<string, OpenAPISchema> = {
BitflagStringType: {type: 'string', format: 'int64', pattern: '^[0-9]+$'},
ColorType: {type: 'integer', minimum: 0, maximum: 16777215, format: 'int32'},
Int32Type: {type: 'integer', minimum: 0, maximum: 2147483647, format: 'int32'},
NonNegativeSafeIntegerType: {type: 'integer', minimum: 0, maximum: 9007199254740991, format: 'int53'},
EmailType: {type: 'string', format: 'email'},
PasswordType: {type: 'string', minLength: 8, maxLength: 256},
UsernameType: {type: 'string', minLength: 1, maxLength: 32, pattern: '^[a-zA-Z0-9_]+$'},
@@ -8,6 +8,7 @@ import {
Int64StringTypeSchema,
Int64TypeSchema,
LocaleSchema,
NonNegativeSafeIntegerTypeSchema,
PasswordTypeSchema,
PhoneNumberTypeSchema,
SnowflakeTypeSchema,
@@ -127,6 +128,7 @@ const BUILT_IN_SCHEMAS: ReadonlyArray<readonly [string, OpenAPISchema]> = [
['APIErrorCode', APIErrorCodeSchema],
['SnowflakeType', SnowflakeTypeSchema],
['Int32Type', Int32TypeSchema],
['NonNegativeSafeIntegerType', NonNegativeSafeIntegerTypeSchema],
['Int64Type', Int64TypeSchema],
['Int64StringType', Int64StringTypeSchema],
['UnsignedInt64Type', UnsignedInt64TypeSchema],
@@ -253,6 +253,9 @@ export class OpenAPIOperationBuilder {
if (typeString.includes('UnsignedInt64Type')) {
return {type: 'string', format: 'int64', pattern: '^[0-9]+$'};
}
if (typeString.includes('NonNegativeSafeIntegerType')) {
return {type: 'integer', minimum: 0, maximum: 9007199254740991, format: 'int53'};
}
if (typeString.includes('Int64StringType')) {
return {type: 'string', format: 'int64', pattern: '^-?[0-9]+$'};
}
@@ -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));