Skip to content
Merged

Dev #50

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 73 additions & 0 deletions apps/api/src/routes/v1/guilds/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ import {
getGuildRolePosition,
} from "@repo/auth/permissions"
import { and, count, db, desc, eq, ilike, inArray, schema } from "@repo/db"
import { env } from "@repo/env/server"
import { PRESENCE_ONLINE_USERS_SET_KEY } from "@repo/realtime-types"
import { asc } from "drizzle-orm"
import { HTTPException } from "hono/http-exception"
import * as HttpStatusCodes from "@/lib/helpers/http/status-codes"
import { logger } from "@/lib/logger"
import {
Expand All @@ -21,6 +23,7 @@ import type {
SearchMessagesRoute,
TimeoutGuildMemberRoute,
UpdateGuildMemberRoleRoute,
UpdateGuildRoute,
} from "@/routes/v1/guilds/routes"

const PRESENCE_MEMBERSHIP_CHUNK_SIZE = 250
Expand Down Expand Up @@ -470,6 +473,76 @@ export const clearGuildMemberTimeout: AppRouteHandler<
)
}

// ── Guild Settings ─────────────────────────────────────

export const updateGuild: AppRouteHandler<UpdateGuildRoute> = async (c) => {
const guild = c.var.guild
const actor = c.var.member

assertGuildPermission(actor, guild, {
organization: ["update"],
})

const body = c.req.valid("json")

const guildIconPrefix = `${env.S3_PUBLIC_URL.replace(/\/$/, "")}/guild-icons/${guild.id}/`
if (body.logo && !body.logo.startsWith(guildIconPrefix)) {
throw new HTTPException(HttpStatusCodes.BAD_REQUEST, {
message: "Invalid logo URL",
})
}
Comment on lines +488 to +493
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Normalize the logo URL before enforcing the guild prefix.

Lines 488-489 compare the raw string with startsWith(). A value like .../guild-icons/<guildId>/../other/object.png still passes this check, but URL parsers normalize it to a different path. Validate the normalized URL instead of the raw input.

🛡️ Proposed fix
-  const guildIconPrefix = `${env.S3_PUBLIC_URL.replace(/\/$/, "")}/guild-icons/${guild.id}/`
-  if (body.logo && !body.logo.startsWith(guildIconPrefix)) {
-    throw new HTTPException(HttpStatusCodes.BAD_REQUEST, {
-      message: "Invalid logo URL",
-    })
-  }
+  const publicBaseUrl = new URL(env.S3_PUBLIC_URL.replace(/\/?$/, "/"))
+  const guildIconPrefix = new URL(
+    `guild-icons/${guild.id}/`,
+    publicBaseUrl
+  ).href
+  if (body.logo) {
+    const logoUrl = new URL(body.logo)
+    if (!logoUrl.href.startsWith(guildIconPrefix)) {
+      throw new HTTPException(HttpStatusCodes.BAD_REQUEST, {
+        message: "Invalid logo URL",
+      })
+    }
+  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const guildIconPrefix = `${env.S3_PUBLIC_URL.replace(/\/$/, "")}/guild-icons/${guild.id}/`
if (body.logo && !body.logo.startsWith(guildIconPrefix)) {
throw new HTTPException(HttpStatusCodes.BAD_REQUEST, {
message: "Invalid logo URL",
})
}
const publicBaseUrl = new URL(env.S3_PUBLIC_URL.replace(/\/?$/, "/"))
const guildIconPrefix = new URL(
`guild-icons/${guild.id}/`,
publicBaseUrl
).href
if (body.logo) {
const logoUrl = new URL(body.logo)
if (!logoUrl.href.startsWith(guildIconPrefix)) {
throw new HTTPException(HttpStatusCodes.BAD_REQUEST, {
message: "Invalid logo URL",
})
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/api/src/routes/v1/guilds/handlers.ts` around lines 488 - 493, The
current check compares the raw body.logo string with guildIconPrefix causing
paths containing "../" to bypass validation; update the validation in the
handler to parse and normalize the incoming logo URL (use the URL constructor)
and compare the normalized origin+pathname (ensure env.S3_PUBLIC_URL is
normalized without trailing slash and the pathname is compared after URL
pathname normalization) against guildIconPrefix; if URL parsing fails or the
normalized URL does not start with the expected prefix (constructed with
guild.id), throw the existing HTTPException(HttpStatusCodes.BAD_REQUEST, {
message: "Invalid logo URL" }).


const updates: Record<string, unknown> = {}
if (body.name !== undefined) updates.name = body.name
if (body.logo !== undefined) updates.logo = body.logo
Comment thread
coderabbitai[bot] marked this conversation as resolved.

if (Object.keys(updates).length === 0) {
return c.json(
{
success: true as const,
guild: {
id: guild.id,
name: guild.name,
slug: guild.slug,
logo: guild.logo,
},
},
HttpStatusCodes.OK
)
}

const [updated] = await db
.update(schema.guild)
.set(updates)
.where(eq(schema.guild.id, guild.id))
.returning({
id: schema.guild.id,
name: schema.guild.name,
slug: schema.guild.slug,
logo: schema.guild.logo,
})

if (!updated) {
return c.json(
{ success: false, message: "Guild not found" },
HttpStatusCodes.NOT_FOUND
)
}

return c.json(
{
success: true as const,
guild: {
id: updated.id,
name: updated.name,
slug: updated.slug,
logo: updated.logo,
},
},
HttpStatusCodes.OK
)
}

// ── Search ──────────────────────────────────────────────

export const searchMessages: AppRouteHandler<SearchMessagesRoute> = async (
Expand Down
1 change: 1 addition & 0 deletions apps/api/src/routes/v1/guilds/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import * as routes from "@/routes/v1/guilds/routes"
const guildsRouter = createRouter()
.openapi(routes.listGuildMembers, handlers.listGuildMembers)
.openapi(routes.searchMessages, handlers.searchMessages)
.openapi(routes.updateGuild, handlers.updateGuild)
.openapi(routes.updateGuildMemberRole, handlers.updateGuildMemberRole)
.openapi(routes.kickGuildMember, handlers.kickGuildMember)
.openapi(routes.banGuildMember, handlers.banGuildMember)
Expand Down
30 changes: 30 additions & 0 deletions apps/api/src/routes/v1/guilds/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import {
timeoutGuildMemberResponseSchema,
updateGuildMemberRoleRequestSchema,
updateGuildMemberRoleResponseSchema,
updateGuildRequestSchema,
updateGuildResponseSchema,
} from "./schema"

export const listGuildMembers = createRoute({
Expand Down Expand Up @@ -210,3 +212,31 @@ export const searchMessages = createRoute({
})

export type SearchMessagesRoute = typeof searchMessages

export const updateGuild = createRoute({
path: "/guilds/{guildSlug}",
method: "patch",
summary: "Update guild settings",
description: "Updates guild name and/or logo. Requires admin or owner role.",
tags: ["Guilds"],
middleware: [guildAuthMiddleware] as const,
request: {
params: guildSlugParamsSchema,
body: jsonContent({
schema: updateGuildRequestSchema,
description: "Guild fields to update",
}),
},
responses: {
[HttpStatusCodes.OK]: jsonContent({
schema: updateGuildResponseSchema,
description: "Updated guild",
}),
[HttpStatusCodes.UNAUTHORIZED]: unauthorizedSchema,
[HttpStatusCodes.FORBIDDEN]: forbiddenSchema,
[HttpStatusCodes.NOT_FOUND]: notFoundSchema,
[HttpStatusCodes.INTERNAL_SERVER_ERROR]: internalServerErrorSchema,
},
})

export type UpdateGuildRoute = typeof updateGuild
21 changes: 21 additions & 0 deletions apps/api/src/routes/v1/guilds/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,27 @@ export const timeoutGuildMemberResponseSchema = z.object({
member: guildMemberPresenceSchema,
})

// ── Guild Settings ─────────────────────────────────────

export const updateGuildRequestSchema = z
.object({
name: z.string().trim().min(1).max(100).optional(),
logo: z.string().url().nullable().optional(),
})
.refine((data) => data.name !== undefined || data.logo !== undefined, {
message: "At least one field (name or logo) must be provided",
})

export const updateGuildResponseSchema = z.object({
success: z.literal(true),
guild: z.object({
id: z.string().uuid(),
name: z.string(),
slug: z.string(),
logo: z.string().nullable(),
}),
})

// ── Search ──────────────────────────────────────────────

export const searchMessagesQuerySchema = paginationQuerySchema.extend({
Expand Down
124 changes: 120 additions & 4 deletions apps/api/src/routes/v1/uploads/handlers.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,31 @@
import { PutObjectCommand } from "@aws-sdk/client-s3"
import { getSignedUrl } from "@aws-sdk/s3-request-presigner"
import {
type GuildRole,
guildAuthorityHasPermissions,
isGuildRole,
} from "@repo/auth/permissions"
import { db } from "@repo/db"
import { channel, channelMember, guildMember } from "@repo/db/schema"
import { channel, channelMember, guild, guildMember } from "@repo/db/schema"
import { env } from "@repo/env/server"
import { and, eq } from "drizzle-orm"
import * as HttpStatusCodes from "@/lib/helpers/http/status-codes"
import { assertMemberCanCommunicate } from "@/lib/permissions"
import {
assertGuildPermission,
assertMemberCanCommunicate,
} from "@/lib/permissions"
import { s3Client } from "@/lib/s3"
import type { AppRouteHandler } from "@/lib/types/app-types"
import type { AvatarPresignRoute, PresignRoute } from "./routes"
import { MAX_AVATAR_SIZE, PRESIGNED_URL_EXPIRY_SECONDS } from "./schema"
import type {
AvatarPresignRoute,
GuildIconPresignRoute,
PresignRoute,
} from "./routes"
import {
MAX_AVATAR_SIZE,
MAX_GUILD_ICON_SIZE,
PRESIGNED_URL_EXPIRY_SECONDS,
} from "./schema"
Comment on lines +19 to +28
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Use @/* aliases for API-local imports.

Both ./routes and ./schema are relative imports inside apps/api. Converting them to @/routes/v1/uploads/routes and @/routes/v1/uploads/schema keeps this file aligned with the package import convention.

♻️ Suggested change
 import type {
   AvatarPresignRoute,
   GuildIconPresignRoute,
   PresignRoute,
-} from "./routes"
+} from "@/routes/v1/uploads/routes"
 import {
   MAX_AVATAR_SIZE,
   MAX_GUILD_ICON_SIZE,
   PRESIGNED_URL_EXPIRY_SECONDS,
-} from "./schema"
+} from "@/routes/v1/uploads/schema"

As per coding guidelines, Use @/* path alias in apps/api for imports (e.g., import foo from "@/lib/foo"). tsc-alias will rewrite these to relative .js paths in the compiled output.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import type {
AvatarPresignRoute,
GuildIconPresignRoute,
PresignRoute,
} from "./routes"
import {
MAX_AVATAR_SIZE,
MAX_GUILD_ICON_SIZE,
PRESIGNED_URL_EXPIRY_SECONDS,
} from "./schema"
import type {
AvatarPresignRoute,
GuildIconPresignRoute,
PresignRoute,
} from "@/routes/v1/uploads/routes"
import {
MAX_AVATAR_SIZE,
MAX_GUILD_ICON_SIZE,
PRESIGNED_URL_EXPIRY_SECONDS,
} from "@/routes/v1/uploads/schema"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/api/src/routes/v1/uploads/handlers.ts` around lines 19 - 28, Change the
two local relative imports to use the API package alias so they follow the
apps/api convention: replace the import of types AvatarPresignRoute,
GuildIconPresignRoute, PresignRoute from "./routes" with
"@/routes/v1/uploads/routes" and replace the constants MAX_AVATAR_SIZE,
MAX_GUILD_ICON_SIZE, PRESIGNED_URL_EXPIRY_SECONDS from "./schema" with
"@/routes/v1/uploads/schema"; keep the same imported symbols
(AvatarPresignRoute, GuildIconPresignRoute, PresignRoute, MAX_AVATAR_SIZE,
MAX_GUILD_ICON_SIZE, PRESIGNED_URL_EXPIRY_SECONDS) so tsc-alias can rewrite them
at build time.


const DM_CHANNEL_TYPES = ["dm", "group_dm"] as const

Expand Down Expand Up @@ -44,6 +60,8 @@ export const presign: AppRouteHandler<PresignRoute> = async (c) => {
const member = await db
.select({
id: guildMember.id,
role: guildMember.role,
userId: guildMember.userId,
communicationDisabledUntil: guildMember.communicationDisabledUntil,
})
.from(guildMember)
Expand All @@ -64,6 +82,39 @@ export const presign: AppRouteHandler<PresignRoute> = async (c) => {
}

assertMemberCanCommunicate(member)

// Block uploads in announcement channels for users without permission
if (ch.type === "announcement") {
if (!isGuildRole(member.role)) {
return c.json(
{ success: false, message: "Forbidden" },
HttpStatusCodes.FORBIDDEN
)
}

const guildRecord = await db
.select({ ownerId: guild.ownerId })
.from(guild)
.where(eq(guild.id, ch.guildId))
.limit(1)
.then((rows) => rows[0])

if (
!guildRecord ||
!guildAuthorityHasPermissions(
{
role: member.role as GuildRole,
isOwner: guildRecord.ownerId === member.userId,
},
{ announcement: ["send"] }
)
) {
return c.json(
{ success: false, message: "Forbidden" },
HttpStatusCodes.FORBIDDEN
)
}
}
Comment on lines +86 to +117
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Don't let unknown roles bypass announcement upload checks.

This branch fails open today: if member.role is not a recognized GuildRole, isGuildRole(member.role) short-circuits the whole announcement permission check and the presign is still issued. Reuse assertGuildPermission(member, guildRecord, { announcement: ["send"] }) or explicitly reject unknown roles so bad role data returns 403 instead of allowing uploads.

🛡️ Suggested change
-    if (ch.type === "announcement" && isGuildRole(member.role)) {
+    if (ch.type === "announcement") {
       const guildRecord = await db
         .select({ ownerId: guild.ownerId })
         .from(guild)
         .where(eq(guild.id, ch.guildId))
         .limit(1)
         .then((rows) => rows[0])

-      if (
-        !guildRecord ||
-        !guildAuthorityHasPermissions(
-          {
-            role: member.role as GuildRole,
-            isOwner: guildRecord.ownerId === member.userId,
-          },
-          { announcement: ["send"] }
-        )
-      ) {
+      if (!guildRecord) {
         return c.json(
           { success: false, message: "Forbidden" },
           HttpStatusCodes.FORBIDDEN
         )
       }
+
+      assertGuildPermission(member, guildRecord, {
+        announcement: ["send"],
+      })
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Block uploads in announcement channels for users without permission
if (ch.type === "announcement" && isGuildRole(member.role)) {
const guildRecord = await db
.select({ ownerId: guild.ownerId })
.from(guild)
.where(eq(guild.id, ch.guildId))
.limit(1)
.then((rows) => rows[0])
if (
!guildRecord ||
!guildAuthorityHasPermissions(
{
role: member.role as GuildRole,
isOwner: guildRecord.ownerId === member.userId,
},
{ announcement: ["send"] }
)
) {
return c.json(
{ success: false, message: "Forbidden" },
HttpStatusCodes.FORBIDDEN
)
}
}
// Block uploads in announcement channels for users without permission
if (ch.type === "announcement") {
const guildRecord = await db
.select({ ownerId: guild.ownerId })
.from(guild)
.where(eq(guild.id, ch.guildId))
.limit(1)
.then((rows) => rows[0])
if (!guildRecord) {
return c.json(
{ success: false, message: "Forbidden" },
HttpStatusCodes.FORBIDDEN
)
}
assertGuildPermission(member, guildRecord, {
announcement: ["send"],
})
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/api/src/routes/v1/uploads/handlers.ts` around lines 86 - 110, The
announcement upload branch currently lets unknown/invalid roles bypass checks
because isGuildRole(member.role) short-circuits; update the logic to reject
unknown roles or reuse the existing guard: fetch guildRecord as shown, then call
assertGuildPermission(member, guildRecord, { announcement: ["send"] }) (or, if
you keep the manual check, explicitly return 403 when isGuildRole(member.role)
is false) so that invalid member.role cannot issue a presign; reference
isGuildRole, guildRecord, member.role, guildAuthorityHasPermissions, and
assertGuildPermission to locate and update the code.

} else if (
DM_CHANNEL_TYPES.includes(ch.type as (typeof DM_CHANNEL_TYPES)[number])
) {
Expand Down Expand Up @@ -143,3 +194,68 @@ export const avatarPresign: AppRouteHandler<AvatarPresignRoute> = async (c) => {

return c.json({ uploadUrl, fileUrl }, HttpStatusCodes.OK)
}

export const guildIconPresign: AppRouteHandler<GuildIconPresignRoute> = async (
c
) => {
const user = c.var.user
const { guildId, filename, contentType, size } = c.req.valid("json")

if (size > MAX_GUILD_ICON_SIZE) {
return c.json(
{ success: false, message: "File too large" },
HttpStatusCodes.REQUEST_TOO_LONG
)
}

// Verify guild exists and user has update permission
const guildRecord = await db
.select({ ownerId: guild.ownerId })
.from(guild)
.where(eq(guild.id, guildId))
.limit(1)
.then((rows) => rows[0])

if (!guildRecord) {
return c.json(
{ success: false, message: "Forbidden" },
HttpStatusCodes.FORBIDDEN
)
}

const member = await db
.select({ role: guildMember.role, userId: guildMember.userId })
.from(guildMember)
.where(
and(eq(guildMember.guildId, guildId), eq(guildMember.userId, user.id))
)
.limit(1)
.then((rows) => rows[0])

if (!member) {
return c.json(
{ success: false, message: "Forbidden" },
HttpStatusCodes.FORBIDDEN
)
}

assertGuildPermission(member, guildRecord, { organization: ["update"] })

const sanitizedFilename = filename.replace(/[^a-zA-Z0-9._-]/g, "_")
const key = `guild-icons/${guildId}/${crypto.randomUUID()}/${sanitizedFilename}`

const command = new PutObjectCommand({
Bucket: env.S3_BUCKET_NAME,
Key: key,
ContentType: contentType,
ContentLength: size,
})

const uploadUrl = await getSignedUrl(s3Client, command, {
expiresIn: PRESIGNED_URL_EXPIRY_SECONDS,
})

const fileUrl = `${env.S3_PUBLIC_URL.replace(/\/$/, "")}/${key}`

return c.json({ uploadUrl, fileUrl }, HttpStatusCodes.OK)
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
1 change: 1 addition & 0 deletions apps/api/src/routes/v1/uploads/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@ import * as routes from "./routes"
const uploadsRouter = createRouter()
.openapi(routes.presign, handlers.presign)
.openapi(routes.avatarPresign, handlers.avatarPresign)
.openapi(routes.guildIconPresign, handlers.guildIconPresign)

export default uploadsRouter
30 changes: 30 additions & 0 deletions apps/api/src/routes/v1/uploads/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import { sessionAuthMiddleware } from "@/middleware/session-auth"
import {
avatarPresignRequestSchema,
avatarPresignResponseSchema,
guildIconPresignRequestSchema,
guildIconPresignResponseSchema,
presignRequestSchema,
presignResponseSchema,
} from "./schema"
Comment on lines 11 to 18
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Use the API alias for this schema import.

./schema is still a relative import inside apps/api. Switch it to @/routes/v1/uploads/schema so this file follows the package-wide alias convention.

♻️ Suggested change
 import {
   avatarPresignRequestSchema,
   avatarPresignResponseSchema,
   guildIconPresignRequestSchema,
   guildIconPresignResponseSchema,
   presignRequestSchema,
   presignResponseSchema,
-} from "./schema"
+} from "@/routes/v1/uploads/schema"

As per coding guidelines, Use @/* path alias in apps/api for imports (e.g., import foo from "@/lib/foo"). tsc-alias will rewrite these to relative .js paths in the compiled output.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import {
avatarPresignRequestSchema,
avatarPresignResponseSchema,
guildIconPresignRequestSchema,
guildIconPresignResponseSchema,
presignRequestSchema,
presignResponseSchema,
} from "./schema"
import {
avatarPresignRequestSchema,
avatarPresignResponseSchema,
guildIconPresignRequestSchema,
guildIconPresignResponseSchema,
presignRequestSchema,
presignResponseSchema,
} from "@/routes/v1/uploads/schema"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/api/src/routes/v1/uploads/routes.ts` around lines 11 - 18, The import of
schema symbols (avatarPresignRequestSchema, avatarPresignResponseSchema,
guildIconPresignRequestSchema, guildIconPresignResponseSchema,
presignRequestSchema, presignResponseSchema) should use the package alias
instead of a relative path; update the import source from "./schema" to
"@/routes/v1/uploads/schema" so the file follows the apps/api `@/`* alias
convention and tsc-alias can rewrite paths at build time.

Expand Down Expand Up @@ -69,3 +71,31 @@ export const avatarPresign = createRoute({
})

export type AvatarPresignRoute = typeof avatarPresign

export const guildIconPresign = createRoute({
path: "/uploads/guild-icon/presign",
method: "post",
summary: "Request a presigned URL for guild icon upload",
description:
"Returns a presigned URL for uploading a guild icon to S3-compatible storage.",
tags: ["Uploads"],
middleware: [sessionAuthMiddleware] as const,
request: {
body: jsonContent({
schema: guildIconPresignRequestSchema,
description: "Guild icon file metadata",
}),
},
responses: {
[HttpStatusCodes.OK]: jsonContent({
schema: guildIconPresignResponseSchema,
description: "Presigned URL for guild icon upload",
}),
[HttpStatusCodes.UNAUTHORIZED]: unauthorizedSchema,
[HttpStatusCodes.FORBIDDEN]: forbiddenSchema,
[HttpStatusCodes.REQUEST_TOO_LONG]: payloadTooLargeSchema,
[HttpStatusCodes.INTERNAL_SERVER_ERROR]: internalServerErrorSchema,
},
})

export type GuildIconPresignRoute = typeof guildIconPresign
Loading
Loading