mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-17 21:15:32 +09:00
fix(api)!: correct declared bounds and hide public bot mfa (#2485)
This commit is contained in:
1 parent
3be4171256
commit
bec34ea147
15 files changed
+222
-41
No files matched your search
@@ -1,5 +1,6 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {MAX_APPLICATION_REDIRECT_URIS} from '@fluxer/constants/src/LimitConstants';
|
||||
import {
|
||||
createStringType,
|
||||
Int32Type,
|
||||
@@ -21,7 +22,10 @@ export const ApplicationAdminResponse = z.object({
|
||||
bot_discriminator: z.string().nullable().describe('The discriminator of the bot user, if any'),
|
||||
bot_is_public: z.boolean().describe('Whether the bot is publicly joinable'),
|
||||
bot_require_code_grant: z.boolean().describe('Whether an OAuth2 code grant is required for this bot'),
|
||||
oauth2_redirect_uris: z.array(z.string()).max(100).describe('Registered OAuth2 redirect URIs'),
|
||||
oauth2_redirect_uris: z
|
||||
.array(z.string())
|
||||
.max(MAX_APPLICATION_REDIRECT_URIS)
|
||||
.describe('Registered OAuth2 redirect URIs'),
|
||||
has_client_secret: z.boolean().describe('Whether a hashed client secret is stored for this application'),
|
||||
has_bot_token: z.boolean().describe('Whether a hashed bot token is stored for this application'),
|
||||
bot_token_preview: z.string().nullable().describe('The preview (last few characters) of the bot token, if any'),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {CONTENT_WARNING_TEXT_MAX_LENGTH} from '@fluxer/constants/src/GuildConstants';
|
||||
import {MAX_GROUP_DM_OTHER_RECIPIENTS} from '@fluxer/constants/src/LimitConstants';
|
||||
import {MAX_GROUP_DM_OTHER_RECIPIENTS, MAX_GROUP_DM_RECIPIENTS} from '@fluxer/constants/src/LimitConstants';
|
||||
import {type UserPartial, UserPartialResponse} from '@fluxer/schema/src/domains/user/UserResponseSchemas';
|
||||
import {ChannelOverwriteTypeSchema, ChannelTypeSchema} from '@fluxer/schema/src/primitives/ChannelValidators';
|
||||
import {ContentWarningLevelSchema} from '@fluxer/schema/src/primitives/GuildValidators';
|
||||
@@ -71,7 +71,6 @@ export const ChannelResponse = z.object({
|
||||
.describe('The ISO 8601 timestamp of when the last pinned message was pinned'),
|
||||
permission_overwrites: z
|
||||
.array(ChannelOverwriteResponse)
|
||||
.max(500)
|
||||
.optional()
|
||||
.describe('The permission overwrites for this channel'),
|
||||
recipients: z
|
||||
@@ -125,7 +124,7 @@ export const ChannelPartialResponse = z.object({
|
||||
type: ChannelTypeSchema.describe('The type of the channel'),
|
||||
recipients: z
|
||||
.array(ChannelPartialRecipientResponse)
|
||||
.max(MAX_GROUP_DM_OTHER_RECIPIENTS)
|
||||
.max(MAX_GROUP_DM_RECIPIENTS)
|
||||
.optional()
|
||||
.describe('The recipients of the DM channel'),
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {MAX_GUILD_STICKER_TAGS} from '@fluxer/constants/src/LimitConstants';
|
||||
import {type UserPartial, UserPartialResponse} from '@fluxer/schema/src/domains/user/UserResponseSchemas';
|
||||
import {SnowflakeStringType} from '@fluxer/schema/src/primitives/SchemaPrimitives';
|
||||
import {z} from 'zod';
|
||||
@@ -27,7 +28,7 @@ export const GuildStickerResponse = z.object({
|
||||
id: SnowflakeStringType.describe('The unique identifier for this sticker'),
|
||||
name: z.string().describe('The name of the sticker'),
|
||||
description: z.string().describe('The description of the sticker'),
|
||||
tags: z.array(z.string()).max(100).describe('Autocomplete/suggestion tags for the sticker'),
|
||||
tags: z.array(z.string()).max(MAX_GUILD_STICKER_TAGS).describe('Autocomplete/suggestion tags for the sticker'),
|
||||
animated: z.boolean().describe('Whether this sticker is animated'),
|
||||
nsfw: z.boolean().describe('Deprecated; always false. Retained for compatibility with older clients'),
|
||||
});
|
||||
@@ -38,7 +39,7 @@ export const GuildStickerWithUserResponse = z.object({
|
||||
id: SnowflakeStringType.describe('The unique identifier for this sticker'),
|
||||
name: z.string().describe('The name of the sticker'),
|
||||
description: z.string().describe('The description of the sticker'),
|
||||
tags: z.array(z.string()).max(100).describe('Autocomplete/suggestion tags for the sticker'),
|
||||
tags: z.array(z.string()).max(MAX_GUILD_STICKER_TAGS).describe('Autocomplete/suggestion tags for the sticker'),
|
||||
animated: z.boolean().describe('Whether this sticker is animated'),
|
||||
nsfw: z.boolean().describe('Deprecated; always false. Retained for compatibility with older clients'),
|
||||
user: z.lazy(() => UserPartialResponse).describe('The user who uploaded this sticker'),
|
||||
|
||||
@@ -10,6 +10,8 @@ import {
|
||||
import {
|
||||
AVATAR_MAX_SIZE,
|
||||
EMOJI_MAX_SIZE,
|
||||
MAX_GUILD_ROLES,
|
||||
MAX_GUILD_STICKER_TAGS,
|
||||
MAX_TEMP_BAN_DURATION_SECONDS,
|
||||
MIN_TEMP_BAN_DURATION_SECONDS,
|
||||
STICKER_MAX_SIZE,
|
||||
@@ -142,10 +144,10 @@ export const GuildMemberUpdateRequest = z.object({
|
||||
.describe('The nickname to set for the member (1-32 characters)'),
|
||||
roles: z
|
||||
.array(SnowflakeType)
|
||||
.max(100, 'Maximum 100 roles allowed')
|
||||
.max(MAX_GUILD_ROLES, `Maximum ${MAX_GUILD_ROLES} roles allowed`)
|
||||
.optional()
|
||||
.transform((ids) => (ids ? new Set(ids) : undefined))
|
||||
.describe('Array of role IDs to assign to the member (max 100)'),
|
||||
.describe(`Array of role IDs to assign to the member (max ${MAX_GUILD_ROLES})`),
|
||||
avatar: createBase64StringType(1, Math.ceil(AVATAR_MAX_SIZE * (4 / 3)))
|
||||
.nullish()
|
||||
.describe('Base64-encoded image data for the member guild avatar'),
|
||||
@@ -241,10 +243,10 @@ export const GuildStickerCreateRequest = z.object({
|
||||
tags: z
|
||||
.array(createStringType(1, 30))
|
||||
.min(0)
|
||||
.max(10)
|
||||
.max(MAX_GUILD_STICKER_TAGS)
|
||||
.optional()
|
||||
.default([])
|
||||
.describe('Array of autocomplete/suggestion tags (max 10 tags, each 1-30 characters)'),
|
||||
.describe(`Array of autocomplete/suggestion tags (max ${MAX_GUILD_STICKER_TAGS} tags, each 1-30 characters)`),
|
||||
image: createBase64StringType(1, Math.ceil(STICKER_MAX_SIZE * (4 / 3))).describe(
|
||||
'Base64-encoded image data for the sticker',
|
||||
),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {ApplicationFlags, BotFlags, BotFlagsDescriptions} from '@fluxer/constants/src/BotConstants';
|
||||
import {AVATAR_MAX_SIZE} from '@fluxer/constants/src/LimitConstants';
|
||||
import {AVATAR_MAX_SIZE, MAX_APPLICATION_REDIRECT_URIS} from '@fluxer/constants/src/LimitConstants';
|
||||
import {
|
||||
PublicUserFlags,
|
||||
PublicUserFlagsDescriptions,
|
||||
@@ -176,7 +176,10 @@ const ApplicationBotResponse = z
|
||||
export const ApplicationResponse = z.object({
|
||||
id: SnowflakeStringType.describe('The unique identifier of the application'),
|
||||
name: z.string().describe('The name of the application'),
|
||||
redirect_uris: z.array(z.string()).max(20).describe('The registered redirect URIs for OAuth2'),
|
||||
redirect_uris: z
|
||||
.array(z.string())
|
||||
.max(MAX_APPLICATION_REDIRECT_URIS)
|
||||
.describe('The registered redirect URIs for OAuth2'),
|
||||
bot_public: z.boolean().describe('Whether the bot can be invited by anyone'),
|
||||
bot_require_code_grant: z.boolean().describe('Whether the bot requires OAuth2 code grant'),
|
||||
client_secret: z.string().optional().describe('The client secret for OAuth2 authentication'),
|
||||
@@ -295,10 +298,16 @@ export const ApplicationPublicResponse = z.object({
|
||||
name: z.string().describe('The name of the application'),
|
||||
icon: z.string().nullable().describe('The icon hash of the application'),
|
||||
description: z.string().nullable().describe('The description of the application'),
|
||||
redirect_uris: z.array(z.string()).max(20).describe('The registered redirect URIs for OAuth2'),
|
||||
redirect_uris: z
|
||||
.array(z.string())
|
||||
.max(MAX_APPLICATION_REDIRECT_URIS)
|
||||
.describe('The registered redirect URIs for OAuth2'),
|
||||
scopes: z.array(z.string()).max(50).describe('The available OAuth2 scopes'),
|
||||
bot_public: z.boolean().describe('Whether the bot can be invited by anyone'),
|
||||
bot: ApplicationBotResponse.nullable().describe('The bot user associated with the application'),
|
||||
bot: ApplicationBotResponse.omit({mfa_enabled: true, authenticator_types: true})
|
||||
.describe('Detailed bot user metadata')
|
||||
.nullable()
|
||||
.describe('The bot user associated with the application'),
|
||||
current_user: UserPartialResponse.nullable()
|
||||
.optional()
|
||||
.describe('Partial user data for the authenticated requester, when a session token is present'),
|
||||
@@ -316,7 +325,11 @@ export const ApplicationsMeResponse = z.object({
|
||||
verify_key: z.string().describe('Compatibility placeholder for AppInfo clients until keys are persisted'),
|
||||
owner: UserPartialResponse.describe('The owner of the application'),
|
||||
bot: ApplicationBotResponse.optional().describe('The bot user associated with the application'),
|
||||
redirect_uris: z.array(z.string()).max(20).optional().describe('The registered redirect URIs for OAuth2'),
|
||||
redirect_uris: z
|
||||
.array(z.string())
|
||||
.max(MAX_APPLICATION_REDIRECT_URIS)
|
||||
.optional()
|
||||
.describe('The registered redirect URIs for OAuth2'),
|
||||
});
|
||||
|
||||
export type ApplicationsMeResponse = z.infer<typeof ApplicationsMeResponse>;
|
||||
@@ -406,7 +419,7 @@ export const ApplicationCreateRequest = z.object({
|
||||
name: createStringType(1, 100).describe('The name of the application'),
|
||||
redirect_uris: z
|
||||
.array(OAuth2RedirectURICreateType)
|
||||
.max(10, 'Maximum of 10 redirect URIs allowed')
|
||||
.max(MAX_APPLICATION_REDIRECT_URIS, `Maximum of ${MAX_APPLICATION_REDIRECT_URIS} redirect URIs allowed`)
|
||||
.optional()
|
||||
.nullable()
|
||||
.transform((value) => value ?? [])
|
||||
@@ -421,7 +434,7 @@ export const ApplicationUpdateRequest = z.object({
|
||||
name: createStringType(1, 100).optional().describe('The name of the application'),
|
||||
redirect_uris: z
|
||||
.array(OAuth2RedirectURIUpdateType)
|
||||
.max(10, 'Maximum of 10 redirect URIs allowed')
|
||||
.max(MAX_APPLICATION_REDIRECT_URIS, `Maximum of ${MAX_APPLICATION_REDIRECT_URIS} redirect URIs allowed`)
|
||||
.optional()
|
||||
.nullable()
|
||||
.transform((value) => (value === undefined ? undefined : (value ?? [])))
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {MAX_GROUP_DM_RECIPIENTS} from '@fluxer/constants/src/LimitConstants';
|
||||
import {
|
||||
ChannelOverwriteResponse,
|
||||
ChannelPartialResponse,
|
||||
@@ -105,6 +106,22 @@ describe('ChannelResponse', () => {
|
||||
expect(result.data.permission_overwrites).toHaveLength(2);
|
||||
}
|
||||
});
|
||||
it('accepts more permission_overwrites than the old response ceiling', () => {
|
||||
const channel = {
|
||||
...validChannel,
|
||||
permission_overwrites: Array.from({length: 501}, (_, index) => ({
|
||||
id: String(100000000000000000n + BigInt(index)),
|
||||
type: 0,
|
||||
allow: '8',
|
||||
deny: '0',
|
||||
})),
|
||||
};
|
||||
const result = ChannelResponse.safeParse(channel);
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) {
|
||||
expect(result.data.permission_overwrites).toHaveLength(501);
|
||||
}
|
||||
});
|
||||
it('accepts voice channel properties', () => {
|
||||
const voiceChannel = {
|
||||
...validChannel,
|
||||
@@ -221,4 +238,23 @@ describe('ChannelPartialResponse', () => {
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
it('accepts a full group DM with every recipient listed', () => {
|
||||
const result = ChannelPartialResponse.safeParse({
|
||||
id: '123456789012345678',
|
||||
type: 3,
|
||||
recipients: Array.from({length: MAX_GROUP_DM_RECIPIENTS}, (_, index) => ({username: `user${index}`})),
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) {
|
||||
expect(result.data.recipients).toHaveLength(MAX_GROUP_DM_RECIPIENTS);
|
||||
}
|
||||
});
|
||||
it('rejects more recipients than a group DM can hold', () => {
|
||||
const result = ChannelPartialResponse.safeParse({
|
||||
id: '123456789012345678',
|
||||
type: 3,
|
||||
recipients: Array.from({length: MAX_GROUP_DM_RECIPIENTS + 1}, (_, index) => ({username: `user${index}`})),
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,23 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {MAX_GUILD_STICKER_TAGS} from '@fluxer/constants/src/LimitConstants';
|
||||
import {GuildStickerResponse} from '@fluxer/schema/src/domains/guild/GuildEmojiSchemas';
|
||||
import {describe, expect, it} from 'vitest';
|
||||
|
||||
const buildSticker = (tagCount: number) => ({
|
||||
id: '1234567890123456789',
|
||||
name: 'sticker',
|
||||
description: 'A sticker',
|
||||
tags: Array.from({length: tagCount}, (_, index) => `tag${index}`),
|
||||
animated: false,
|
||||
nsfw: false,
|
||||
});
|
||||
|
||||
describe('GuildStickerResponse', () => {
|
||||
it('accepts the maximum tag count', () => {
|
||||
expect(GuildStickerResponse.safeParse(buildSticker(MAX_GUILD_STICKER_TAGS)).success).toBe(true);
|
||||
});
|
||||
it('rejects one tag beyond the maximum', () => {
|
||||
expect(GuildStickerResponse.safeParse(buildSticker(MAX_GUILD_STICKER_TAGS + 1)).success).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -1,7 +1,16 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {MAX_TEMP_BAN_DURATION_SECONDS, MIN_TEMP_BAN_DURATION_SECONDS} from '@fluxer/constants/src/LimitConstants';
|
||||
import {GuildBanCreateRequest} from '@fluxer/schema/src/domains/guild/GuildRequestSchemas';
|
||||
import {
|
||||
MAX_GUILD_ROLES,
|
||||
MAX_GUILD_STICKER_TAGS,
|
||||
MAX_TEMP_BAN_DURATION_SECONDS,
|
||||
MIN_TEMP_BAN_DURATION_SECONDS,
|
||||
} from '@fluxer/constants/src/LimitConstants';
|
||||
import {
|
||||
GuildBanCreateRequest,
|
||||
GuildMemberUpdateRequest,
|
||||
GuildStickerCreateRequest,
|
||||
} from '@fluxer/schema/src/domains/guild/GuildRequestSchemas';
|
||||
import {describe, expect, it} from 'vitest';
|
||||
|
||||
describe('GuildBanCreateRequest', () => {
|
||||
@@ -20,3 +29,31 @@ describe('GuildBanCreateRequest', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
const buildRoleIds = (count: number) =>
|
||||
Array.from({length: count}, (_, index) => `${1234567890123456789n + BigInt(index)}`);
|
||||
|
||||
describe('GuildMemberUpdateRequest', () => {
|
||||
it('accepts every role a member can hold', () => {
|
||||
expect(GuildMemberUpdateRequest.safeParse({roles: buildRoleIds(MAX_GUILD_ROLES)}).success).toBe(true);
|
||||
});
|
||||
it('rejects one role beyond the guild role limit', () => {
|
||||
expect(GuildMemberUpdateRequest.safeParse({roles: buildRoleIds(MAX_GUILD_ROLES + 1)}).success).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('GuildStickerCreateRequest', () => {
|
||||
const buildTags = (count: number) => Array.from({length: count}, (_, index) => `tag${index}`);
|
||||
const image = `data:image/png;base64,${Buffer.from('sticker').toString('base64')}`;
|
||||
it('accepts the maximum tag count', () => {
|
||||
expect(
|
||||
GuildStickerCreateRequest.safeParse({name: 'sticker', tags: buildTags(MAX_GUILD_STICKER_TAGS), image}).success,
|
||||
).toBe(true);
|
||||
});
|
||||
it('rejects one tag beyond the maximum', () => {
|
||||
expect(
|
||||
GuildStickerCreateRequest.safeParse({name: 'sticker', tags: buildTags(MAX_GUILD_STICKER_TAGS + 1), image})
|
||||
.success,
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,29 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {MAX_APPLICATION_REDIRECT_URIS} from '@fluxer/constants/src/LimitConstants';
|
||||
import {ApplicationCreateRequest, ApplicationResponse} from '@fluxer/schema/src/domains/oauth/OAuthSchemas';
|
||||
import {describe, expect, it} from 'vitest';
|
||||
|
||||
const buildRedirectURIs = (count: number) =>
|
||||
Array.from({length: count}, (_, index) => `https://example.com/callback/${index}`);
|
||||
|
||||
const buildApplicationResponse = (redirectURIs: Array<string>) => ({
|
||||
id: '1234567890123456789',
|
||||
name: 'Test Application',
|
||||
redirect_uris: redirectURIs,
|
||||
bot_public: true,
|
||||
bot_require_code_grant: false,
|
||||
});
|
||||
|
||||
describe('application redirect_uris bounds', () => {
|
||||
it('accepts the maximum redirect URI count on both the request and the response', () => {
|
||||
const uris = buildRedirectURIs(MAX_APPLICATION_REDIRECT_URIS);
|
||||
expect(ApplicationCreateRequest.safeParse({name: 'Test Application', redirect_uris: uris}).success).toBe(true);
|
||||
expect(ApplicationResponse.safeParse(buildApplicationResponse(uris)).success).toBe(true);
|
||||
});
|
||||
it('rejects one redirect URI beyond the maximum on both the request and the response', () => {
|
||||
const uris = buildRedirectURIs(MAX_APPLICATION_REDIRECT_URIS + 1);
|
||||
expect(ApplicationCreateRequest.safeParse({name: 'Test Application', redirect_uris: uris}).success).toBe(false);
|
||||
expect(ApplicationResponse.safeParse(buildApplicationResponse(uris)).success).toBe(false);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user