@jgardner04/ghost-mcp-server 1.11.0 → 1.12.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,316 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import fs from 'fs';
3
+ import fsPromises from 'fs/promises';
4
+
5
+ // Mock fs modules before importing the module under test
6
+ vi.mock('fs', async () => {
7
+ const actual = await vi.importActual('fs');
8
+ return {
9
+ ...actual,
10
+ default: {
11
+ ...actual,
12
+ unlinkSync: vi.fn(),
13
+ },
14
+ unlinkSync: vi.fn(),
15
+ };
16
+ });
17
+
18
+ vi.mock('fs/promises', () => ({
19
+ default: {
20
+ unlink: vi.fn(),
21
+ },
22
+ unlink: vi.fn(),
23
+ }));
24
+
25
+ // Import after mocking
26
+ import {
27
+ trackTempFile,
28
+ untrackTempFile,
29
+ cleanupTempFile,
30
+ cleanupTempFiles,
31
+ cleanupAllTrackedFilesSync,
32
+ getTrackedFiles,
33
+ clearTracking,
34
+ } from '../tempFileManager.js';
35
+
36
+ describe('tempFileManager', () => {
37
+ beforeEach(() => {
38
+ // Clear tracking and reset mocks before each test
39
+ clearTracking();
40
+ vi.clearAllMocks();
41
+ });
42
+
43
+ describe('trackTempFile', () => {
44
+ it('should add file path to tracking set', () => {
45
+ trackTempFile('/tmp/test-file.jpg');
46
+
47
+ const tracked = getTrackedFiles();
48
+ expect(tracked.has('/tmp/test-file.jpg')).toBe(true);
49
+ });
50
+
51
+ it('should handle null gracefully', () => {
52
+ expect(() => trackTempFile(null)).not.toThrow();
53
+ expect(getTrackedFiles().size).toBe(0);
54
+ });
55
+
56
+ it('should handle undefined gracefully', () => {
57
+ expect(() => trackTempFile(undefined)).not.toThrow();
58
+ expect(getTrackedFiles().size).toBe(0);
59
+ });
60
+
61
+ it('should handle empty string gracefully', () => {
62
+ expect(() => trackTempFile('')).not.toThrow();
63
+ expect(getTrackedFiles().size).toBe(0);
64
+ });
65
+
66
+ it('should not add duplicate paths', () => {
67
+ trackTempFile('/tmp/test-file.jpg');
68
+ trackTempFile('/tmp/test-file.jpg');
69
+
70
+ const tracked = getTrackedFiles();
71
+ expect(tracked.size).toBe(1);
72
+ });
73
+
74
+ it('should track multiple different files', () => {
75
+ trackTempFile('/tmp/file1.jpg');
76
+ trackTempFile('/tmp/file2.jpg');
77
+ trackTempFile('/tmp/file3.jpg');
78
+
79
+ const tracked = getTrackedFiles();
80
+ expect(tracked.size).toBe(3);
81
+ });
82
+ });
83
+
84
+ describe('untrackTempFile', () => {
85
+ it('should remove file path from tracking set', () => {
86
+ trackTempFile('/tmp/test-file.jpg');
87
+ untrackTempFile('/tmp/test-file.jpg');
88
+
89
+ const tracked = getTrackedFiles();
90
+ expect(tracked.has('/tmp/test-file.jpg')).toBe(false);
91
+ });
92
+
93
+ it('should handle non-existent paths gracefully', () => {
94
+ expect(() => untrackTempFile('/tmp/non-existent.jpg')).not.toThrow();
95
+ });
96
+
97
+ it('should handle null gracefully', () => {
98
+ expect(() => untrackTempFile(null)).not.toThrow();
99
+ });
100
+
101
+ it('should handle undefined gracefully', () => {
102
+ expect(() => untrackTempFile(undefined)).not.toThrow();
103
+ });
104
+ });
105
+
106
+ describe('cleanupTempFile', () => {
107
+ it('should delete file and untrack it', async () => {
108
+ fsPromises.unlink.mockResolvedValueOnce(undefined);
109
+ trackTempFile('/tmp/test-file.jpg');
110
+
111
+ await cleanupTempFile('/tmp/test-file.jpg');
112
+
113
+ expect(fsPromises.unlink).toHaveBeenCalledWith('/tmp/test-file.jpg');
114
+ expect(getTrackedFiles().has('/tmp/test-file.jpg')).toBe(false);
115
+ });
116
+
117
+ it('should resolve when file does not exist (ENOENT)', async () => {
118
+ const error = new Error('File not found');
119
+ error.code = 'ENOENT';
120
+ fsPromises.unlink.mockRejectedValueOnce(error);
121
+
122
+ await expect(cleanupTempFile('/tmp/non-existent.jpg')).resolves.toBeUndefined();
123
+ });
124
+
125
+ it('should not log warning for ENOENT errors', async () => {
126
+ const error = new Error('File not found');
127
+ error.code = 'ENOENT';
128
+ fsPromises.unlink.mockRejectedValueOnce(error);
129
+ const mockLogger = { warn: vi.fn() };
130
+
131
+ await cleanupTempFile('/tmp/non-existent.jpg', mockLogger);
132
+
133
+ expect(mockLogger.warn).not.toHaveBeenCalled();
134
+ });
135
+
136
+ it('should log warning on other errors but still resolve', async () => {
137
+ const error = new Error('Permission denied');
138
+ error.code = 'EACCES';
139
+ fsPromises.unlink.mockRejectedValueOnce(error);
140
+ const mockLogger = { warn: vi.fn() };
141
+
142
+ await expect(cleanupTempFile('/tmp/test.jpg', mockLogger)).resolves.toBeUndefined();
143
+ expect(mockLogger.warn).toHaveBeenCalledWith(
144
+ 'Failed to delete temp file',
145
+ expect.objectContaining({ file: '/tmp/test.jpg' })
146
+ );
147
+ });
148
+
149
+ it('should handle null path', async () => {
150
+ await expect(cleanupTempFile(null)).resolves.toBeUndefined();
151
+ expect(fsPromises.unlink).not.toHaveBeenCalled();
152
+ });
153
+
154
+ it('should handle undefined path', async () => {
155
+ await expect(cleanupTempFile(undefined)).resolves.toBeUndefined();
156
+ expect(fsPromises.unlink).not.toHaveBeenCalled();
157
+ });
158
+
159
+ it('should handle empty string path', async () => {
160
+ await expect(cleanupTempFile('')).resolves.toBeUndefined();
161
+ expect(fsPromises.unlink).not.toHaveBeenCalled();
162
+ });
163
+
164
+ it('should untrack file even on error', async () => {
165
+ const error = new Error('Permission denied');
166
+ error.code = 'EACCES';
167
+ fsPromises.unlink.mockRejectedValueOnce(error);
168
+ trackTempFile('/tmp/test.jpg');
169
+ const mockLogger = { warn: vi.fn() };
170
+
171
+ await cleanupTempFile('/tmp/test.jpg', mockLogger);
172
+
173
+ expect(getTrackedFiles().has('/tmp/test.jpg')).toBe(false);
174
+ });
175
+
176
+ it('should work with default console logger', async () => {
177
+ fsPromises.unlink.mockResolvedValueOnce(undefined);
178
+
179
+ // Should not throw when using default logger
180
+ await expect(cleanupTempFile('/tmp/test.jpg')).resolves.toBeUndefined();
181
+ });
182
+ });
183
+
184
+ describe('cleanupTempFiles', () => {
185
+ it('should clean up multiple files in parallel', async () => {
186
+ fsPromises.unlink.mockResolvedValue(undefined);
187
+
188
+ await cleanupTempFiles(['/tmp/file1.jpg', '/tmp/file2.jpg', '/tmp/file3.jpg']);
189
+
190
+ expect(fsPromises.unlink).toHaveBeenCalledTimes(3);
191
+ expect(fsPromises.unlink).toHaveBeenCalledWith('/tmp/file1.jpg');
192
+ expect(fsPromises.unlink).toHaveBeenCalledWith('/tmp/file2.jpg');
193
+ expect(fsPromises.unlink).toHaveBeenCalledWith('/tmp/file3.jpg');
194
+ });
195
+
196
+ it('should handle mixed success/failure scenarios', async () => {
197
+ fsPromises.unlink
198
+ .mockResolvedValueOnce(undefined)
199
+ .mockRejectedValueOnce(new Error('Failed'))
200
+ .mockResolvedValueOnce(undefined);
201
+ const mockLogger = { warn: vi.fn() };
202
+
203
+ // Should resolve even with failures
204
+ await expect(
205
+ cleanupTempFiles(['/tmp/file1.jpg', '/tmp/file2.jpg', '/tmp/file3.jpg'], mockLogger)
206
+ ).resolves.toBeUndefined();
207
+ });
208
+
209
+ it('should filter out null and undefined paths', async () => {
210
+ fsPromises.unlink.mockResolvedValue(undefined);
211
+
212
+ await cleanupTempFiles(['/tmp/file1.jpg', null, undefined, '/tmp/file2.jpg']);
213
+
214
+ expect(fsPromises.unlink).toHaveBeenCalledTimes(2);
215
+ });
216
+
217
+ it('should filter out empty string paths', async () => {
218
+ fsPromises.unlink.mockResolvedValue(undefined);
219
+
220
+ await cleanupTempFiles(['/tmp/file1.jpg', '', '/tmp/file2.jpg']);
221
+
222
+ expect(fsPromises.unlink).toHaveBeenCalledTimes(2);
223
+ });
224
+
225
+ it('should deduplicate paths', async () => {
226
+ fsPromises.unlink.mockResolvedValue(undefined);
227
+
228
+ await cleanupTempFiles(['/tmp/file1.jpg', '/tmp/file1.jpg', '/tmp/file2.jpg']);
229
+
230
+ expect(fsPromises.unlink).toHaveBeenCalledTimes(2);
231
+ });
232
+
233
+ it('should handle empty file list', async () => {
234
+ await expect(cleanupTempFiles([])).resolves.toBeUndefined();
235
+ expect(fsPromises.unlink).not.toHaveBeenCalled();
236
+ });
237
+
238
+ it('should handle list with only null/undefined', async () => {
239
+ await expect(cleanupTempFiles([null, undefined, ''])).resolves.toBeUndefined();
240
+ expect(fsPromises.unlink).not.toHaveBeenCalled();
241
+ });
242
+ });
243
+
244
+ describe('cleanupAllTrackedFilesSync', () => {
245
+ it('should clean up all currently tracked files', () => {
246
+ fs.unlinkSync.mockImplementation(() => {});
247
+ trackTempFile('/tmp/file1.jpg');
248
+ trackTempFile('/tmp/file2.jpg');
249
+
250
+ cleanupAllTrackedFilesSync();
251
+
252
+ expect(fs.unlinkSync).toHaveBeenCalledWith('/tmp/file1.jpg');
253
+ expect(fs.unlinkSync).toHaveBeenCalledWith('/tmp/file2.jpg');
254
+ });
255
+
256
+ it('should clear the tracking set after cleanup', () => {
257
+ fs.unlinkSync.mockImplementation(() => {});
258
+ trackTempFile('/tmp/file1.jpg');
259
+
260
+ cleanupAllTrackedFilesSync();
261
+
262
+ expect(getTrackedFiles().size).toBe(0);
263
+ });
264
+
265
+ it('should handle empty tracking set gracefully', () => {
266
+ expect(() => cleanupAllTrackedFilesSync()).not.toThrow();
267
+ });
268
+
269
+ it('should continue cleanup even if one file fails', () => {
270
+ fs.unlinkSync
271
+ .mockImplementationOnce(() => {
272
+ throw new Error('Failed');
273
+ })
274
+ .mockImplementationOnce(() => {});
275
+ trackTempFile('/tmp/file1.jpg');
276
+ trackTempFile('/tmp/file2.jpg');
277
+
278
+ expect(() => cleanupAllTrackedFilesSync()).not.toThrow();
279
+ expect(fs.unlinkSync).toHaveBeenCalledTimes(2);
280
+ });
281
+
282
+ it('should clear tracking set even on errors', () => {
283
+ fs.unlinkSync.mockImplementation(() => {
284
+ throw new Error('Failed');
285
+ });
286
+ trackTempFile('/tmp/file1.jpg');
287
+
288
+ cleanupAllTrackedFilesSync();
289
+
290
+ expect(getTrackedFiles().size).toBe(0);
291
+ });
292
+ });
293
+
294
+ describe('getTrackedFiles', () => {
295
+ it('should return a copy of the tracking set', () => {
296
+ trackTempFile('/tmp/test.jpg');
297
+
298
+ const tracked = getTrackedFiles();
299
+ tracked.add('/tmp/hacked.jpg'); // Try to modify
300
+
301
+ // Original set should not be affected
302
+ expect(getTrackedFiles().has('/tmp/hacked.jpg')).toBe(false);
303
+ });
304
+ });
305
+
306
+ describe('clearTracking', () => {
307
+ it('should clear all tracked files', () => {
308
+ trackTempFile('/tmp/file1.jpg');
309
+ trackTempFile('/tmp/file2.jpg');
310
+
311
+ clearTracking();
312
+
313
+ expect(getTrackedFiles().size).toBe(0);
314
+ });
315
+ });
316
+ });
@@ -0,0 +1,163 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { z } from 'zod';
3
+ import { validateToolInput } from '../validation.js';
4
+
5
+ describe('validateToolInput', () => {
6
+ const testSchema = z.object({
7
+ name: z.string().min(1),
8
+ count: z.number().int().positive(),
9
+ });
10
+
11
+ describe('valid input', () => {
12
+ it('should return success with validated data for valid input', () => {
13
+ const result = validateToolInput(testSchema, { name: 'test', count: 5 }, 'test_tool');
14
+
15
+ expect(result.success).toBe(true);
16
+ expect(result.data).toEqual({ name: 'test', count: 5 });
17
+ });
18
+
19
+ it('should not include errorResponse on success', () => {
20
+ const result = validateToolInput(testSchema, { name: 'test', count: 5 }, 'test_tool');
21
+
22
+ expect(result.success).toBe(true);
23
+ expect(result).not.toHaveProperty('errorResponse');
24
+ });
25
+ });
26
+
27
+ describe('invalid input', () => {
28
+ it('should return error response for invalid input', () => {
29
+ const result = validateToolInput(testSchema, { name: '', count: -1 }, 'test_tool');
30
+
31
+ expect(result.success).toBe(false);
32
+ expect(result.errorResponse.isError).toBe(true);
33
+ expect(result.errorResponse.content[0].type).toBe('text');
34
+ });
35
+
36
+ it('should include tool name in error context', () => {
37
+ const result = validateToolInput(testSchema, {}, 'ghost_create_tag');
38
+
39
+ expect(result.success).toBe(false);
40
+ const errorText = result.errorResponse.content[0].text;
41
+ expect(errorText).toContain('ghost_create_tag');
42
+ expect(errorText).toContain('Validation failed');
43
+ });
44
+
45
+ it('should include VALIDATION_ERROR code in response', () => {
46
+ const result = validateToolInput(testSchema, { name: '' }, 'test_tool');
47
+
48
+ expect(result.success).toBe(false);
49
+ const errorText = result.errorResponse.content[0].text;
50
+ expect(errorText).toContain('VALIDATION_ERROR');
51
+ });
52
+
53
+ it('should include structured error information', () => {
54
+ const result = validateToolInput(testSchema, { name: '' }, 'test_tool');
55
+
56
+ expect(result.success).toBe(false);
57
+ const errorObj = JSON.parse(result.errorResponse.content[0].text);
58
+ expect(errorObj.name).toBe('ValidationError');
59
+ expect(errorObj.code).toBe('VALIDATION_ERROR');
60
+ expect(errorObj.statusCode).toBe(400);
61
+ expect(errorObj.message).toContain('Validation failed');
62
+ });
63
+ });
64
+
65
+ describe('edge cases', () => {
66
+ it('should handle undefined input', () => {
67
+ const result = validateToolInput(testSchema, undefined, 'test_tool');
68
+
69
+ expect(result.success).toBe(false);
70
+ expect(result.errorResponse.isError).toBe(true);
71
+ });
72
+
73
+ it('should handle null input', () => {
74
+ const result = validateToolInput(testSchema, null, 'test_tool');
75
+
76
+ expect(result.success).toBe(false);
77
+ expect(result.errorResponse.isError).toBe(true);
78
+ });
79
+
80
+ it('should handle empty object input', () => {
81
+ const result = validateToolInput(testSchema, {}, 'test_tool');
82
+
83
+ expect(result.success).toBe(false);
84
+ expect(result.errorResponse.isError).toBe(true);
85
+ });
86
+ });
87
+
88
+ describe('schema transforms', () => {
89
+ it('should apply schema transforms and return transformed data', () => {
90
+ const transformSchema = z.object({
91
+ value: z.string().transform((s) => s.toUpperCase()),
92
+ });
93
+ const result = validateToolInput(transformSchema, { value: 'test' }, 'test_tool');
94
+
95
+ expect(result.success).toBe(true);
96
+ expect(result.data.value).toBe('TEST');
97
+ });
98
+
99
+ it('should apply default values from schema', () => {
100
+ const defaultSchema = z.object({
101
+ name: z.string(),
102
+ enabled: z.boolean().default(true),
103
+ });
104
+ const result = validateToolInput(defaultSchema, { name: 'test' }, 'test_tool');
105
+
106
+ expect(result.success).toBe(true);
107
+ expect(result.data.enabled).toBe(true);
108
+ });
109
+ });
110
+
111
+ describe('partial schemas', () => {
112
+ it('should work with partial schemas', () => {
113
+ const partialSchema = testSchema.partial();
114
+ const result = validateToolInput(partialSchema, {}, 'test_tool');
115
+
116
+ expect(result.success).toBe(true);
117
+ expect(result.data).toEqual({});
118
+ });
119
+
120
+ it('should still validate provided fields in partial schemas', () => {
121
+ const partialSchema = testSchema.partial();
122
+ const result = validateToolInput(partialSchema, { count: -1 }, 'test_tool');
123
+
124
+ expect(result.success).toBe(false);
125
+ expect(result.errorResponse.isError).toBe(true);
126
+ });
127
+ });
128
+
129
+ describe('refinement schemas', () => {
130
+ it('should handle schema refinements and return error', () => {
131
+ const refinedSchema = z
132
+ .object({
133
+ id: z.string().optional(),
134
+ slug: z.string().optional(),
135
+ })
136
+ .refine((data) => data.id || data.slug, {
137
+ message: 'Either id or slug is required',
138
+ });
139
+
140
+ const result = validateToolInput(refinedSchema, {}, 'test_tool');
141
+
142
+ expect(result.success).toBe(false);
143
+ expect(result.errorResponse.isError).toBe(true);
144
+ const errorObj = JSON.parse(result.errorResponse.content[0].text);
145
+ expect(errorObj.code).toBe('VALIDATION_ERROR');
146
+ });
147
+
148
+ it('should pass validation when refinement is satisfied', () => {
149
+ const refinedSchema = z
150
+ .object({
151
+ id: z.string().optional(),
152
+ slug: z.string().optional(),
153
+ })
154
+ .refine((data) => data.id || data.slug, {
155
+ message: 'Either id or slug is required',
156
+ });
157
+
158
+ const result = validateToolInput(refinedSchema, { id: 'abc123' }, 'test_tool');
159
+
160
+ expect(result.success).toBe(true);
161
+ });
162
+ });
163
+ });
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Temp File Manager
3
+ *
4
+ * Provides robust temp file tracking and cleanup with:
5
+ * - Async cleanup using fs.promises.unlink with await
6
+ * - Parallel cleanup using Promise.allSettled
7
+ * - Process exit handlers for orphaned file cleanup
8
+ */
9
+ import fs from 'fs';
10
+ import fsPromises from 'fs/promises';
11
+
12
+ // Global Set to track temp files for cleanup
13
+ const tempFilesToCleanup = new Set();
14
+
15
+ /**
16
+ * Track a temp file for cleanup
17
+ * @param {string|null|undefined} filePath - Path to track
18
+ */
19
+ export const trackTempFile = (filePath) => {
20
+ if (filePath) {
21
+ tempFilesToCleanup.add(filePath);
22
+ }
23
+ };
24
+
25
+ /**
26
+ * Untrack a temp file (remove from cleanup tracking)
27
+ * @param {string|null|undefined} filePath - Path to untrack
28
+ */
29
+ export const untrackTempFile = (filePath) => {
30
+ if (filePath) {
31
+ tempFilesToCleanup.delete(filePath);
32
+ }
33
+ };
34
+
35
+ /**
36
+ * Clean up a single temp file asynchronously
37
+ * @param {string|null|undefined} filePath - Path to delete
38
+ * @param {object} logger - Logger with warn method (default: console)
39
+ * @returns {Promise<void>}
40
+ */
41
+ export const cleanupTempFile = async (filePath, logger = console) => {
42
+ if (!filePath) return;
43
+
44
+ try {
45
+ await fsPromises.unlink(filePath);
46
+ } catch (err) {
47
+ // ENOENT means file already deleted - not an error
48
+ if (err.code !== 'ENOENT') {
49
+ logger.warn?.('Failed to delete temp file', { file: filePath, error: err.message });
50
+ }
51
+ }
52
+ // Always untrack, even on error
53
+ untrackTempFile(filePath);
54
+ };
55
+
56
+ /**
57
+ * Clean up multiple temp files in parallel
58
+ * @param {Array<string|null|undefined>} filePaths - Paths to delete
59
+ * @param {object} logger - Logger with warn method (default: console)
60
+ * @returns {Promise<void>}
61
+ */
62
+ export const cleanupTempFiles = async (filePaths, logger = console) => {
63
+ // Filter out falsy values and deduplicate
64
+ const uniquePaths = [...new Set(filePaths.filter(Boolean))];
65
+
66
+ if (uniquePaths.length === 0) return;
67
+
68
+ // Use Promise.allSettled to ensure all cleanups attempt even if some fail
69
+ await Promise.allSettled(uniquePaths.map((p) => cleanupTempFile(p, logger)));
70
+ };
71
+
72
+ /**
73
+ * Synchronously clean up all currently tracked files
74
+ * Used for process exit handlers where async is not possible
75
+ */
76
+ export const cleanupAllTrackedFilesSync = () => {
77
+ for (const file of tempFilesToCleanup) {
78
+ try {
79
+ fs.unlinkSync(file);
80
+ } catch (_) {
81
+ // Ignore errors during exit cleanup
82
+ }
83
+ }
84
+ tempFilesToCleanup.clear();
85
+ };
86
+
87
+ /**
88
+ * Get a copy of currently tracked files (for testing)
89
+ * @returns {Set<string>}
90
+ */
91
+ export const getTrackedFiles = () => new Set(tempFilesToCleanup);
92
+
93
+ /**
94
+ * Clear all tracking (for testing)
95
+ */
96
+ export const clearTracking = () => {
97
+ tempFilesToCleanup.clear();
98
+ };
99
+
100
+ // Register process exit handlers
101
+ // 'exit' event fires synchronously, so we must use sync cleanup
102
+ process.on('exit', cleanupAllTrackedFilesSync);
103
+
104
+ // SIGINT (Ctrl+C) and SIGTERM need to call process.exit to trigger the 'exit' handler
105
+ process.on('SIGINT', () => {
106
+ cleanupAllTrackedFilesSync();
107
+ process.exit(0);
108
+ });
109
+
110
+ process.on('SIGTERM', () => {
111
+ cleanupAllTrackedFilesSync();
112
+ process.exit(0);
113
+ });
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Validation utilities for MCP tool handlers
3
+ * Provides explicit Zod validation to ensure input is validated at handler entry points.
4
+ */
5
+ import { ValidationError } from '../errors/index.js';
6
+
7
+ /**
8
+ * Validates tool input against a Zod schema and returns a structured result.
9
+ *
10
+ * @param {import('zod').ZodSchema} schema - The Zod schema to validate against
11
+ * @param {unknown} input - The raw input to validate
12
+ * @param {string} toolName - The tool name for error context
13
+ * @returns {{ success: true, data: unknown } | { success: false, errorResponse: object }}
14
+ */
15
+ export const validateToolInput = (schema, input, toolName) => {
16
+ const result = schema.safeParse(input);
17
+ if (!result.success) {
18
+ const error = ValidationError.fromZod(result.error, toolName);
19
+ return {
20
+ success: false,
21
+ errorResponse: {
22
+ content: [{ type: 'text', text: JSON.stringify(error.toJSON(), null, 2) }],
23
+ isError: true,
24
+ },
25
+ };
26
+ }
27
+ return { success: true, data: result.data };
28
+ };