diff --git a/server/api/admin.ts b/server/api/admin.ts index 71853b9c5..004b35479 100644 --- a/server/api/admin.ts +++ b/server/api/admin.ts @@ -244,6 +244,11 @@ export const agentAnalysisQuerySchema = z.object({ workspaceExternalId: z.string().optional(), }) +// Schema for connector ID parameter +export const connectorIdParamsSchema = z.object({ + connectorId: z.string().min(1), +}) + export const GetConnectors = async (c: Context) => { const { workspaceId, sub } = c.get(JwtPayloadKey) const users: SelectUser[] = await getUserByEmail(db, sub) diff --git a/server/api/agent.ts b/server/api/agent.ts index c1c7477a3..a77eff44f 100644 --- a/server/api/agent.ts +++ b/server/api/agent.ts @@ -131,6 +131,15 @@ export const listAgentsSchema = z.object({ filter: z.enum(["all", "madeByMe", "sharedToMe"]).optional().default("all"), }) +export const getAgentParamsSchema = z.object({ + agentExternalId: z.string().min(1), +}) + +export const generatePromptQuerySchema = z.object({ + requirements: z.string().min(1, "Requirements are required"), + modelId: z.string().optional(), +}) + export const safeGet = (c: Context, key: string): T | undefined => { try { return c.get(key) as T @@ -514,8 +523,8 @@ export const GetWorkspaceUsersApi = async (c: Context) => { .where( and( eq(users.workspaceId, userAndWorkspace.workspace.id), - isNull(users.deletedAt) - ) + isNull(users.deletedAt), + ), ) return c.json(workspaceUsers) diff --git a/server/api/dataSource.ts b/server/api/dataSource.ts index 2dafe7c8e..12f235714 100644 --- a/server/api/dataSource.ts +++ b/server/api/dataSource.ts @@ -232,6 +232,18 @@ export const deleteDocumentSchema = z.object({ schema: z.string().min(1), }) +export const getDataSourceFileParamsSchema = z.object({ + docId: z.string().min(1), +}) + +export const listDataSourceFilesParamsSchema = z.object({ + dataSourceName: z.string().min(1), +}) + +export const getAgentsForDataSourceParamsSchema = z.object({ + dataSourceId: z.string().min(1), +}) + export const DeleteImages = async (docId: string) => { const imageDir = path.resolve( process.env.IMAGE_DIR || "downloads/xyne_images_db", diff --git a/server/api/knowledgeBase.ts b/server/api/knowledgeBase.ts index 786d301c1..b3552b2e3 100644 --- a/server/api/knowledgeBase.ts +++ b/server/api/knowledgeBase.ts @@ -123,6 +123,13 @@ const MAX_ZIP_FILE_SIZE = 35 // 35MB max zip file size } })() +// Duplicate handling strategies +enum DuplicateStrategy { + SKIP = "skip", + RENAME = "rename", + OVERWRITE = "overwrite", +} + // Schema definitions for Knowledge Base feature const createCollectionSchema = z.object({ name: z.string().min(1).max(255), @@ -144,6 +151,84 @@ const createFolderSchema = z.object({ metadata: z.record(z.any(), z.any()).optional(), }) +const pollCollectionsStatusSchema = z.object({ + collectionIds: z.array(z.string()).min(1), +}) + +const collectionParamsSchema = z.object({ + clId: z.string().min(1), +}) + +const collectionNameForSharedAgentParamsSchema = z.object({ + clId: z.string().min(1), +}) + +const collectionNameForSharedAgentQuerySchema = z.object({ + agentExternalId: z.string().min(1), +}) + +const listCollectionItemsParamsSchema = z.object({ + clId: z.string().min(1), +}) + +const listCollectionItemsQuerySchema = z.object({ + parentId: z.string().optional(), +}) + +const deleteItemParamsSchema = z.object({ + clId: z.string().min(1), + itemId: z.string().min(1), +}) + +const fileOperationParamsSchema = z.object({ + clId: z.string().min(1), + itemId: z.string().min(1), +}) + +const chunkContentParamsSchema = z.object({ + cId: z.string().min(1), + docId: z.string().min(1), +}) + +const listCollectionsQuerySchema = z.object({ + ownOnly: z.string().optional(), + includeItems: z.string().optional(), +}) + +// Form schemas for file upload endpoints +// Note: 'files' and 'paths' fields are validated manually in the handler +// because they are File objects and string arrays that can't be effectively +// validated with Zod schemas (size, MIME type, binary content validation required) +const uploadFilesFormSchema = z.object({ + parentId: z.string().optional().nullable(), + duplicateStrategy: z + .enum([ + DuplicateStrategy.SKIP, + DuplicateStrategy.RENAME, + DuplicateStrategy.OVERWRITE, + ]) + .optional(), + sessionId: z.string().optional().nullable(), +}) + +// Export schemas for use in server.ts +export { + createCollectionSchema, + updateCollectionSchema, + createFolderSchema, + pollCollectionsStatusSchema, + collectionParamsSchema, + collectionNameForSharedAgentParamsSchema, + collectionNameForSharedAgentQuerySchema, + listCollectionItemsParamsSchema, + listCollectionItemsQuerySchema, + deleteItemParamsSchema, + fileOperationParamsSchema, + chunkContentParamsSchema, + listCollectionsQuerySchema, + uploadFilesFormSchema, +} + // Helper functions function calculateChecksum(buffer: ArrayBuffer): string { const hash = crypto.createHash("sha256") @@ -952,13 +1037,6 @@ export const CreateFolderApi = async (c: Context) => { } } -// Duplicate handling strategies -enum DuplicateStrategy { - SKIP = "skip", - RENAME = "rename", - OVERWRITE = "overwrite", -} - // Helper function to generate unique name function generateUniqueName(baseName: string, existingNames: string[]): string { const nameLower = baseName.toLowerCase() diff --git a/server/api/search.ts b/server/api/search.ts index e3af3d212..65256cd24 100644 --- a/server/api/search.ts +++ b/server/api/search.ts @@ -224,10 +224,13 @@ export const messageSchema = z.object({ if (!val) return false return val.toLowerCase() === "true" }), - isFollowUp: z.string().optional().transform((val) => { - if (!val) return false - return val.toLowerCase() === "true" - }), + isFollowUp: z + .string() + .optional() + .transform((val) => { + if (!val) return false + return val.toLowerCase() === "true" + }), }) export type MessageReqType = z.infer @@ -255,6 +258,13 @@ export const generatePromptSchema = z.object({ ), }) +export const getDriveItemSchema = z.object({ + parentId: z.string().optional(), +}) + +export const getDriveItemsByDocIdsSchema = z.object({ + docIds: z.array(z.string()).min(1, "At least one docId is required"), +}) export const handleAttachmentDeleteSchema = z.object({ attachment: attachmentMetadataSchema, @@ -306,7 +316,7 @@ export const SearchApi = async (c: Context) => { debug, agentId, // @ts-ignore - } = c.req.valid("query") + } = c.req.valid("query") let groupCount: any = {} let results: VespaSearchResponse = {} as VespaSearchResponse const timestampRange = getTimestamp(lastUpdated) diff --git a/server/api/testEmail.ts b/server/api/testEmail.ts index a6250795f..2c1bf2a83 100644 --- a/server/api/testEmail.ts +++ b/server/api/testEmail.ts @@ -2,20 +2,28 @@ import { type Context } from "hono" import { emailService } from "@/services/emailService" import { getLogger } from "@/logger" import { Subsystem } from "@/types" +import { z } from "zod" const Logger = getLogger(Subsystem.Server) +export const sendEmailSchema = z.object({ + email: z.string(), + subject: z.string().optional(), + body: z.string().optional(), +}) + export const sendMailHelper = async (c: Context) => { try { Logger.info("Testing email sending...") if (process.env.NODE_ENV !== "production") { - Logger.debug("SES env debug", { - awsAccessKeyIdPrefix: process.env.SES_AWS_ACCESS_KEY_ID?.slice(0, 4) ?? "unset", - awsRegion: process.env.SES_AWS_REGION ?? "unset", - sesFromEmail: process.env.SES_FROM_EMAIL ?? "unset", - }) - } + Logger.debug("SES env debug", { + awsAccessKeyIdPrefix: + process.env.SES_AWS_ACCESS_KEY_ID?.slice(0, 4) ?? "unset", + awsRegion: process.env.SES_AWS_REGION ?? "unset", + sesFromEmail: process.env.SES_FROM_EMAIL ?? "unset", + }) + } const { email, body, subject } = await c.req.json() if (!email) { diff --git a/server/api/tuning.ts b/server/api/tuning.ts index 19954f2d3..8503f5780 100644 --- a/server/api/tuning.ts +++ b/server/api/tuning.ts @@ -203,6 +203,16 @@ export const evaluateSchema = z.object({ // Add other controllable parameters here in the future }) +// Zod schema for delete dataset endpoint +export const deleteDatasetParamsSchema = z.object({ + filename: z.string().min(1), +}) + +// Zod schema for tuning WebSocket route +export const tuningWsParamsSchema = z.object({ + jobId: z.string().min(1), +}) + // --- Helper Functions Adapted from Script --- // Fetch random document from Vespa (adapted for API context) diff --git a/server/server.ts b/server/server.ts index 67c67d034..1186c25cc 100644 --- a/server/server.ts +++ b/server/server.ts @@ -22,6 +22,8 @@ import { GetDriveItem, GetDriveItemsByDocIds, handleAttachmentDeleteSchema, + getDriveItemSchema, + getDriveItemsByDocIdsSchema, } from "@/api/search" import { callNotificationService } from "@/services/callNotifications" import { zValidator } from "@hono/zod-validator" @@ -45,6 +47,11 @@ import { microsoftServiceSchema, UserRoleChangeSchema, chatIdParamSchema, + fileUploadSchema, + attachmentUploadSchema, + fileServeParamsSchema, + getUserWorkspaceInfoQuerySchema, + deleteUserApiKeyParamsSchema, } from "@/types" import { AddApiKeyConnector, @@ -74,6 +81,7 @@ import { adminQuerySchema, userAgentLeaderboardQuerySchema, agentAnalysisQuerySchema, + connectorIdParamsSchema, AddServiceConnectionMicrosoft, UpdateUser, ListAllLoggedInUsers, @@ -151,6 +159,9 @@ import { deleteDocumentSchema, GetAgentsForDataSourceApi, GetDataSourceFile, + getDataSourceFileParamsSchema, + listDataSourceFilesParamsSchema, + getAgentsForDataSourceParamsSchema, } from "@/api/dataSource" import { ChatBookmarkApi, @@ -201,6 +212,9 @@ import { TuningWsRoute, tuneDatasetSchema, DeleteDatasetHandler, + evaluateSchema, + deleteDatasetParamsSchema, + tuningWsParamsSchema, } from "@/api/tuning" import { CreateAgentApi, @@ -214,6 +228,8 @@ import { listAgentsSchema, updateAgentSchema, GetAgentApi, + getAgentParamsSchema, + generatePromptQuerySchema, } from "@/api/agent" import { GeneratePromptApi } from "@/api/agent/promptGeneration" import { @@ -282,6 +298,20 @@ import { GetChunkContentApi, GetCollectionNameForSharedAgentApi, PollCollectionsStatusApi, + createCollectionSchema, + updateCollectionSchema, + createFolderSchema, + pollCollectionsStatusSchema, + collectionParamsSchema, + collectionNameForSharedAgentParamsSchema, + collectionNameForSharedAgentQuerySchema, + listCollectionItemsParamsSchema, + listCollectionItemsQuerySchema, + deleteItemParamsSchema, + fileOperationParamsSchema, + chunkContentParamsSchema, + listCollectionsQuerySchema, + uploadFilesFormSchema, } from "@/api/knowledgeBase" import { searchKnowledgeBaseSchema, @@ -304,7 +334,7 @@ import { updateWorkflowToolSchema, addStepToWorkflowSchema, } from "./db/schema" -import { sendMailHelper } from "@/api/testEmail" +import { sendMailHelper, sendEmailSchema } from "@/api/testEmail" import { emailService } from "./services/emailService" import { AgentMessageApi } from "./api/chat/agents" import { eq } from "drizzle-orm" @@ -925,10 +955,22 @@ export const AppRoutes = app zValidator("json", autocompleteSchema), AutocompleteApi, ) - .post("files/upload", handleFileUpload) - .post("/files/upload-attachment", handleAttachmentUpload) - .get("/attachments/:fileId", handleAttachmentServe) - .get("/attachments/:fileId/thumbnail", handleThumbnailServe) + .post("files/upload", zValidator("form", fileUploadSchema), handleFileUpload) + .post( + "/files/upload-attachment", + zValidator("form", attachmentUploadSchema), + handleAttachmentUpload, + ) + .get( + "/attachments/:fileId", + zValidator("param", fileServeParamsSchema), + handleAttachmentServe, + ) + .get( + "/attachments/:fileId/thumbnail", + zValidator("param", fileServeParamsSchema), + handleThumbnailServe, + ) .post( "/files/delete", zValidator("json", handleAttachmentDeleteSchema), @@ -1020,18 +1062,38 @@ export const AppRoutes = app zValidator("query", searchSchema), SearchSlackChannels, ) - .get("/me", GetUserWorkspaceInfo) + .get( + "/me", + zValidator("query", getUserWorkspaceInfoQuerySchema), + GetUserWorkspaceInfo, + ) .get("/users/api-keys", GetUserApiKeys) .post( "/users/api-key", zValidator("json", CreateApiKeySchema), GenerateUserApiKey, ) - .delete("/users/api-keys/:keyId", DeleteUserApiKey) + .delete( + "/users/api-keys/:keyId", + zValidator("param", deleteUserApiKeyParamsSchema), + DeleteUserApiKey, + ) .get("/datasources", ListDataSourcesApi) - .get("/datasources/:docId", GetDataSourceFile) - .get("/datasources/:dataSourceName/files", ListDataSourceFilesApi) - .get("/datasources/:dataSourceId/agents", GetAgentsForDataSourceApi) + .get( + "/datasources/:docId", + zValidator("param", getDataSourceFileParamsSchema), + GetDataSourceFile, + ) + .get( + "/datasources/:dataSourceName/files", + zValidator("param", listDataSourceFilesParamsSchema), + ListDataSourceFilesApi, + ) + .get( + "/datasources/:dataSourceId/agents", + zValidator("param", getAgentsForDataSourceParamsSchema), + GetAgentsForDataSourceApi, + ) .get("/proxy/:url", ProxyUrl) .get("/answer", zValidator("query", answerSchema), AnswerApi) .post( @@ -1039,17 +1101,33 @@ export const AppRoutes = app zValidator("json", deleteDocumentSchema), DeleteDocumentApi, ) - .post("/search/driveitem", GetDriveItem) - .post("/search/driveitemsbydocids", GetDriveItemsByDocIds) - .post("/tuning/evaluate", EvaluateHandler) + .post( + "/search/driveitem", + zValidator("json", getDriveItemSchema), + GetDriveItem, + ) + .post( + "/search/driveitemsbydocids", + zValidator("json", getDriveItemsByDocIdsSchema), + GetDriveItemsByDocIds, + ) + .post("/tuning/evaluate", zValidator("json", evaluateSchema), EvaluateHandler) .get("/tuning/datasets", ListDatasetsHandler) .post( "/tuning/tuneDataset", zValidator("json", tuneDatasetSchema), TuneDatasetHandler, ) - .delete("/tuning/datasets/:filename", DeleteDatasetHandler) - .get("/tuning/ws/:jobId", TuningWsRoute) + .delete( + "/tuning/datasets/:filename", + zValidator("param", deleteDatasetParamsSchema), + DeleteDatasetHandler, + ) + .get( + "/tuning/ws/:jobId", + zValidator("param", tuningWsParamsSchema), + TuningWsRoute, + ) // Workflow Routes .post( @@ -1122,9 +1200,17 @@ export const AppRoutes = app // Agent Routes .post("/agent/create", zValidator("json", createAgentSchema), CreateAgentApi) - .get("/agent/generate-prompt", GeneratePromptApi) + .get( + "/agent/generate-prompt", + zValidator("query", generatePromptQuerySchema), + GeneratePromptApi, + ) .get("/agents", zValidator("query", listAgentsSchema), ListAgentsApi) - .get("/agent/:agentExternalId", GetAgentApi) + .get( + "/agent/:agentExternalId", + zValidator("param", getAgentParamsSchema), + GetAgentApi, + ) .get("/workspace/users", GetWorkspaceUsersApi) .get( "/workspace/users/search", @@ -1149,48 +1235,125 @@ export const AppRoutes = app .get("/calls/history", GetCallHistoryApi) // Direct message routes .post("/messages/send", zValidator("json", sendMessageSchema), SendMessageApi) - .get("/messages/conversation", zValidator("query", getConversationSchema), GetConversationApi) + .get( + "/messages/conversation", + zValidator("query", getConversationSchema), + GetConversationApi, + ) .post( "/messages/mark-read", zValidator("json", markAsReadSchema), MarkMessagesAsReadApi, ) .get("/messages/unread-counts", GetUnreadCountsApi) - .get("/agent/:agentExternalId/permissions", GetAgentPermissionsApi) - .get("/agent/:agentExternalId/integration-items", GetAgentIntegrationItemsApi) + .get( + "/agent/:agentExternalId/permissions", + zValidator("param", getAgentParamsSchema), + GetAgentPermissionsApi, + ) + .get( + "/agent/:agentExternalId/integration-items", + zValidator("param", getAgentParamsSchema), + GetAgentIntegrationItemsApi, + ) .put( "/agent/:agentExternalId", zValidator("json", updateAgentSchema), UpdateAgentApi, ) - .delete("/agent/:agentExternalId", DeleteAgentApi) + .delete( + "/agent/:agentExternalId", + zValidator("param", getAgentParamsSchema), + DeleteAgentApi, + ) .post("/auth/logout", LogOut) //send Email Route - .post("/email/send", sendMailHelper) + .post("/email/send", zValidator("json", sendEmailSchema), sendMailHelper) // Collection Routes - .post("/cl", CreateCollectionApi) - .get("/cl", ListCollectionsApi) + .post("/cl", zValidator("json", createCollectionSchema), CreateCollectionApi) + .get( + "/cl", + zValidator("query", listCollectionsQuerySchema), + ListCollectionsApi, + ) .get( "/cl/search", zValidator("query", searchKnowledgeBaseSchema), SearchKnowledgeBaseApi, ) - .post("/cl/poll-status", PollCollectionsStatusApi) - .get("/cl/:clId", GetCollectionApi) - .get("/cl/:clId/name", GetCollectionNameForSharedAgentApi) - .put("/cl/:clId", UpdateCollectionApi) - .delete("/cl/:clId", DeleteCollectionApi) - .get("/cl/:clId/items", ListCollectionItemsApi) - .post("/cl/:clId/items/folder", CreateFolderApi) - .post("/cl/:clId/items/upload", UploadFilesApi) + .post( + "/cl/poll-status", + zValidator("json", pollCollectionsStatusSchema), + PollCollectionsStatusApi, + ) + .get( + "/cl/:clId", + zValidator("param", collectionParamsSchema), + GetCollectionApi, + ) + .get( + "/cl/:clId/name", + zValidator("param", collectionNameForSharedAgentParamsSchema), + zValidator("query", collectionNameForSharedAgentQuerySchema), + GetCollectionNameForSharedAgentApi, + ) + .put( + "/cl/:clId", + zValidator("param", collectionParamsSchema), + zValidator("json", updateCollectionSchema), + UpdateCollectionApi, + ) + .delete( + "/cl/:clId", + zValidator("param", collectionParamsSchema), + DeleteCollectionApi, + ) + .get( + "/cl/:clId/items", + zValidator("param", listCollectionItemsParamsSchema), + zValidator("query", listCollectionItemsQuerySchema), + ListCollectionItemsApi, + ) + .post( + "/cl/:clId/items/folder", + zValidator("param", collectionParamsSchema), + zValidator("json", createFolderSchema), + CreateFolderApi, + ) + .post( + "/cl/:clId/items/upload", + zValidator("param", collectionParamsSchema), + zValidator("form", uploadFilesFormSchema), + UploadFilesApi, + ) .post("/cl/:clId/items/upload/batch", UploadFilesApi) // Batch upload endpoint .post("/cl/:clId/items/upload/complete", UploadFilesApi) // Complete batch session - .delete("/cl/:clId/items/:itemId", DeleteItemApi) - .get("/cl/:clId/files/:itemId/preview", GetFilePreviewApi) - .get("/cl/:clId/files/:itemId/content", GetFileContentApi) - .get("/cl/:clId/files/:itemId/download", DownloadFileApi) - .get("/chunk/:cId/files/:docId/content", GetChunkContentApi) + .delete( + "/cl/:clId/items/:itemId", + zValidator("param", deleteItemParamsSchema), + DeleteItemApi, + ) + .get( + "/cl/:clId/files/:itemId/preview", + zValidator("param", fileOperationParamsSchema), + GetFilePreviewApi, + ) + .get( + "/cl/:clId/files/:itemId/content", + zValidator("param", fileOperationParamsSchema), + GetFileContentApi, + ) + .get( + "/cl/:clId/files/:itemId/download", + zValidator("param", fileOperationParamsSchema), + DownloadFileApi, + ) + .get( + "/chunk/:cId/files/:docId/content", + zValidator("param", chunkContentParamsSchema), + GetChunkContentApi, + ) .post( "/oauth/create", @@ -1310,7 +1473,11 @@ export const AppRoutes = app AddStdioMCPConnector, ) - .get("/connector/:connectorId/tools", GetConnectorTools) // Added route for GetConnectorTools + .get( + "/connector/:connectorId/tools", + zValidator("param", connectorIdParamsSchema), + GetConnectorTools, + ) // Added route for GetConnectorTools .delete( "/connector/delete", @@ -1457,11 +1624,19 @@ app zValidator("json", updateAgentSchema), // Update Agent UpdateAgentApi, ) - .delete("/agent/:agentExternalId", DeleteAgentApi) // Delete Agent + .delete( + "/agent/:agentExternalId", + zValidator("param", getAgentParamsSchema), + DeleteAgentApi, + ) // Delete Agent .get("/agent/:agentExternalId", GetAgentApi) // Get Agent details .get("/chat/history", zValidator("query", chatHistorySchema), ChatHistory) // List chat history - .post("/cl", CreateCollectionApi) // Create collection (KB) - .get("/cl", ListCollectionsApi) // List all collections + .post("/cl", zValidator("json", createCollectionSchema), CreateCollectionApi) // Create collection (KB) + .get( + "/cl", + zValidator("query", listCollectionsQuerySchema), + ListCollectionsApi, + ) // List all collections .get( "/cl/search", zValidator("query", searchKnowledgeBaseSchema), // Search over KB @@ -1469,9 +1644,22 @@ app ) .get("/cl/:clId", GetCollectionApi) // Get collection by ID .put("/cl/:clId", UpdateCollectionApi) // Update collection (rename, etc.) - .delete("/cl/:clId", DeleteCollectionApi) // Delete collection (KB) - .post("/cl/:clId/items/upload", UploadFilesApi) // Upload files to KB (supports zip files) - .delete("/cl/:clId/items/:itemId", DeleteItemApi) // Delete Item in KB by ID + .delete( + "/cl/:clId", + zValidator("param", collectionParamsSchema), + DeleteCollectionApi, + ) // Delete collection (KB) + .post( + "/cl/:clId/items/upload", + zValidator("param", collectionParamsSchema), + zValidator("form", uploadFilesFormSchema), + UploadFilesApi, + ) // Upload files to KB + .delete( + "/cl/:clId/items/:itemId", + zValidator("param", deleteItemParamsSchema), + DeleteItemApi, + ) // Delete Item in KB .post("/cl/poll-status", PollCollectionsStatusApi) // Poll collection items status // Proxy function to forward ingestion API calls to sync server diff --git a/server/types.ts b/server/types.ts index 3685a82a3..07d49be65 100644 --- a/server/types.ts +++ b/server/types.ts @@ -655,3 +655,49 @@ export interface DuckDBResult { rows: unknown[][] } } + +export const fileUploadSchema = z.object({ + file: z.any(), + datasourceName: z.string().min(1, "Datasource name is required"), + flag: z.enum(["creation", "addition"], { + message: "Flag must be either 'creation' or 'addition'", + }), + batchIndex: z.string().optional(), + totalBatches: z.string().optional(), +}) + +export type FileUpload = z.infer + +export const attachmentUploadSchema = z.object({ + attachment: z.any(), +}) + +export type AttachmentUpload = z.infer + +export const fileServeParamsSchema = z.object({ + fileId: z.string().min(1, "File ID is required"), +}) + +export type FileServeParams = z.infer + +// User info schemas +export const getUserWorkspaceInfoQuerySchema = z.object({ + timeZone: z.string().optional(), +}) + +export type GetUserWorkspaceInfoQuery = z.infer< + typeof getUserWorkspaceInfoQuerySchema +> + +// Note: GetUserApiKeys doesn't need a schema as it doesn't accept any parameters + +export const deleteUserApiKeyParamsSchema = z.object({ + keyId: z + .string() + .regex(/^\d+$/, "Key ID must be a valid number") + .transform(Number), +}) + +export type DeleteUserApiKeyParams = z.infer< + typeof deleteUserApiKeyParamsSchema +>