From 52df8c0c02db08f1004f0e5addae53be6e5c14fa Mon Sep 17 00:00:00 2001 From: ahmed-n-abdeltwab Date: Wed, 30 Jul 2025 19:17:51 +0300 Subject: [PATCH 1/3] fix: export all from `Ajv` module for broader accessibility --- packages/rest-typings/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/rest-typings/src/index.ts b/packages/rest-typings/src/index.ts index 6bad8905cb4b9..2b1e2e8faaba0 100644 --- a/packages/rest-typings/src/index.ts +++ b/packages/rest-typings/src/index.ts @@ -277,4 +277,4 @@ export * from './v1/banners'; export * from './default'; // Export the ajv instance for use in other packages -export { ajv } from './v1/Ajv'; +export * from './v1/Ajv'; From 69fe9a39ae2cae0111d3847156aab8d951fa32f9 Mon Sep 17 00:00:00 2001 From: ahmed-n-abdeltwab Date: Wed, 30 Jul 2025 19:18:14 +0300 Subject: [PATCH 2/3] refactor: implement standardized error response validation --- apps/meteor/app/api/server/v1/chat.ts | 33 ++--------- apps/meteor/app/api/server/v1/oauthapps.ts | 54 ++++------------- apps/meteor/app/api/server/v1/webdav.ts | 50 ++-------------- packages/rest-typings/src/v1/Ajv.ts | 69 ++++++++++++++++++++++ 4 files changed, 89 insertions(+), 117 deletions(-) diff --git a/apps/meteor/app/api/server/v1/chat.ts b/apps/meteor/app/api/server/v1/chat.ts index 4484fb87cdb7b..525f19ad0bf94 100644 --- a/apps/meteor/app/api/server/v1/chat.ts +++ b/apps/meteor/app/api/server/v1/chat.ts @@ -29,6 +29,8 @@ import { isChatSyncThreadMessagesProps, isChatGetStarredMessagesProps, isChatGetDiscussionsProps, + validateBadRequestErrorResponse, + validateUnauthorizedErrorResponse, } from '@rocket.chat/rest-typings'; import { escapeRegExp } from '@rocket.chat/string-helpers'; import { Meteor } from 'meteor/meteor'; @@ -197,35 +199,8 @@ const chatPinMessageEndpoints = API.v1.post( authRequired: true, body: isChatPinMessageProps, response: { - 400: ajv.compile<{ - error?: string; - errorType?: string; - stack?: string; - details?: string; - }>({ - type: 'object', - properties: { - success: { type: 'boolean', enum: [false] }, - stack: { type: 'string' }, - error: { type: 'string' }, - errorType: { type: 'string' }, - details: { type: 'string' }, - }, - required: ['success'], - additionalProperties: false, - }), - 401: ajv.compile({ - type: 'object', - properties: { - success: { type: 'boolean', enum: [false] }, - status: { type: 'string' }, - message: { type: 'string' }, - error: { type: 'string' }, - errorType: { type: 'string' }, - }, - required: ['success'], - additionalProperties: false, - }), + 400: validateBadRequestErrorResponse, + 401: validateUnauthorizedErrorResponse, 200: ajv.compile<{ message: IMessage }>({ type: 'object', properties: { diff --git a/apps/meteor/app/api/server/v1/oauthapps.ts b/apps/meteor/app/api/server/v1/oauthapps.ts index 2a611337d3500..d154b25ea5770 100644 --- a/apps/meteor/app/api/server/v1/oauthapps.ts +++ b/apps/meteor/app/api/server/v1/oauthapps.ts @@ -1,6 +1,14 @@ import type { IOAuthApps } from '@rocket.chat/core-typings'; import { OAuthApps } from '@rocket.chat/models'; -import { ajv, isUpdateOAuthAppParams, isOauthAppsGetParams, isDeleteOAuthAppParams } from '@rocket.chat/rest-typings'; +import { + ajv, + isUpdateOAuthAppParams, + isOauthAppsGetParams, + isDeleteOAuthAppParams, + validateUnauthorizedErrorResponse, + validateBadRequestErrorResponse, + validateForbiddenErrorResponse, +} from '@rocket.chat/rest-typings'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; import { apiDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; @@ -117,47 +125,9 @@ const oauthAppsCreateEndpoints = API.v1.post( body: isOauthAppsAddParams, permissionsRequired: ['manage-oauth-apps'], response: { - 400: ajv.compile<{ - error?: string; - errorType?: string; - stack?: string; - details?: object; - }>({ - type: 'object', - properties: { - success: { type: 'boolean', enum: [false] }, - stack: { type: 'string' }, - error: { type: 'string' }, - errorType: { type: 'string' }, - details: { type: 'object' }, - }, - required: ['success'], - additionalProperties: false, - }), - 401: ajv.compile({ - type: 'object', - properties: { - success: { type: 'boolean', enum: [false] }, - status: { type: 'string' }, - message: { type: 'string' }, - error: { type: 'string' }, - errorType: { type: 'string' }, - }, - required: ['success'], - additionalProperties: false, - }), - 403: ajv.compile({ - type: 'object', - properties: { - success: { type: 'boolean', enum: [false] }, - status: { type: 'string' }, - message: { type: 'string' }, - error: { type: 'string' }, - errorType: { type: 'string' }, - }, - required: ['success'], - additionalProperties: false, - }), + 400: validateBadRequestErrorResponse, + 401: validateUnauthorizedErrorResponse, + 403: validateForbiddenErrorResponse, 200: ajv.compile<{ application: IOAuthApps }>({ type: 'object', properties: { diff --git a/apps/meteor/app/api/server/v1/webdav.ts b/apps/meteor/app/api/server/v1/webdav.ts index a3d2755b10d73..0a7fedf500bcd 100644 --- a/apps/meteor/app/api/server/v1/webdav.ts +++ b/apps/meteor/app/api/server/v1/webdav.ts @@ -1,7 +1,7 @@ import { api } from '@rocket.chat/core-services'; import type { IWebdavAccount, IWebdavAccountIntegration } from '@rocket.chat/core-typings'; import { WebdavAccounts } from '@rocket.chat/models'; -import { ajv } from '@rocket.chat/rest-typings'; +import { ajv, validateUnauthorizedErrorResponse, validateBadRequestErrorResponse } from '@rocket.chat/rest-typings'; import type { DeleteResult } from 'mongodb'; import type { ExtractRoutesFromAPI } from '../ApiClass'; @@ -48,20 +48,7 @@ const webdavGetMyAccountsEndpoints = API.v1.get( required: ['success', 'accounts'], additionalProperties: false, }), - 401: ajv.compile({ - type: 'object', - properties: { - message: { - type: 'string', - }, - success: { - type: 'boolean', - description: 'Indicates if the request was successful.', - }, - }, - required: ['success', 'message'], - additionalProperties: false, - }), + 401: validateUnauthorizedErrorResponse, }, }, async function action() { @@ -121,37 +108,8 @@ const webdavRemoveAccountEndpoints = API.v1.post( required: ['result', 'success'], additionalProperties: false, }), - 400: ajv.compile({ - type: 'object', - properties: { - errorType: { - type: 'string', - }, - error: { - type: 'string', - }, - success: { - type: 'boolean', - description: 'Indicates if the request was successful.', - }, - }, - required: ['success', 'errorType', 'error'], - additionalProperties: false, - }), - 401: ajv.compile({ - type: 'object', - properties: { - message: { - type: 'string', - }, - success: { - type: 'boolean', - description: 'Indicates if the request was successful.', - }, - }, - required: ['success', 'message'], - additionalProperties: false, - }), + 400: validateBadRequestErrorResponse, + 401: validateUnauthorizedErrorResponse, }, }, async function action() { diff --git a/packages/rest-typings/src/v1/Ajv.ts b/packages/rest-typings/src/v1/Ajv.ts index 5fa88bd1d215c..7d6b47fe98fea 100644 --- a/packages/rest-typings/src/v1/Ajv.ts +++ b/packages/rest-typings/src/v1/Ajv.ts @@ -21,3 +21,72 @@ ajv.addKeyword({ validate: (_schema: unknown, data: unknown): boolean => typeof data === 'string' && !!data.trim(), }); export { ajv }; + +type BadRequestErrorResponse = { + success: false; + error?: string; + errorType?: string; + stack?: string; + details?: string; +}; + +const BadRequestErrorResponseSchema = { + type: 'object', + properties: { + success: { type: 'boolean', enum: [false] }, + stack: { type: 'string' }, + error: { type: 'string' }, + errorType: { type: 'string' }, + details: { type: 'string' }, + }, + required: ['success'], + additionalProperties: false, +}; + +export const validateBadRequestErrorResponse = ajv.compile(BadRequestErrorResponseSchema); + +type UnauthorizedErrorResponse = { + success: false; + status?: string; + message?: string; + error?: string; + errorType?: string; +}; + +const UnauthorizedErrorResponseSchema = { + type: 'object', + properties: { + success: { type: 'boolean', enum: [false] }, + status: { type: 'string' }, + message: { type: 'string' }, + error: { type: 'string' }, + errorType: { type: 'string' }, + }, + required: ['success'], + additionalProperties: false, +}; + +export const validateUnauthorizedErrorResponse = ajv.compile(UnauthorizedErrorResponseSchema); + +type ForbiddenErrorResponse = { + success: false; + status?: string; + message?: string; + error?: string; + errorType?: string; +}; + +const ForbiddenErrorResponseSchema = { + type: 'object', + properties: { + success: { type: 'boolean', enum: [false] }, + status: { type: 'string' }, + message: { type: 'string' }, + error: { type: 'string' }, + errorType: { type: 'string' }, + }, + required: ['success'], + additionalProperties: false, +}; + +export const validateForbiddenErrorResponse = ajv.compile(ForbiddenErrorResponseSchema); From cdeb8f0f2397f1b671d33df17bd8d6e5a59b3329 Mon Sep 17 00:00:00 2001 From: ahmed-n-abdeltwab Date: Wed, 30 Jul 2025 19:54:43 +0300 Subject: [PATCH 3/3] fix: update `details` property to accept both string and object types in error response schemas --- packages/rest-typings/src/v1/Ajv.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/rest-typings/src/v1/Ajv.ts b/packages/rest-typings/src/v1/Ajv.ts index 7d6b47fe98fea..067ac3f820fcf 100644 --- a/packages/rest-typings/src/v1/Ajv.ts +++ b/packages/rest-typings/src/v1/Ajv.ts @@ -27,7 +27,7 @@ type BadRequestErrorResponse = { error?: string; errorType?: string; stack?: string; - details?: string; + details?: string | object; }; const BadRequestErrorResponseSchema = { @@ -37,7 +37,7 @@ const BadRequestErrorResponseSchema = { stack: { type: 'string' }, error: { type: 'string' }, errorType: { type: 'string' }, - details: { type: 'string' }, + details: { anyOf: [{ type: 'string' }, { type: 'object' }] }, }, required: ['success'], additionalProperties: false,