blockmine 1.6.3 → 1.13.0

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.
Files changed (33) hide show
  1. package/.husky/commit-msg +1 -0
  2. package/.husky/pre-commit +1 -0
  3. package/.versionrc.json +17 -0
  4. package/CHANGELOG.md +36 -0
  5. package/README.md +1 -1
  6. package/backend/package.json +1 -0
  7. package/backend/prisma/migrations/20250718181335_add_plugin_data_store/migration.sql +14 -0
  8. package/backend/prisma/schema.prisma +17 -2
  9. package/backend/src/api/routes/auth.js +140 -0
  10. package/backend/src/api/routes/bots.js +176 -0
  11. package/backend/src/api/routes/changelog.js +16 -0
  12. package/backend/src/api/routes/eventGraphs.js +11 -1
  13. package/backend/src/api/routes/plugins.js +11 -0
  14. package/backend/src/core/BotManager.js +92 -40
  15. package/backend/src/core/BotProcess.js +44 -24
  16. package/backend/src/core/EventGraphManager.js +29 -5
  17. package/backend/src/core/GraphExecutionEngine.js +54 -12
  18. package/backend/src/core/MessageQueue.js +10 -1
  19. package/backend/src/core/NodeRegistry.js +2 -1
  20. package/backend/src/core/PluginLoader.js +72 -8
  21. package/backend/src/core/PluginManager.js +19 -0
  22. package/backend/src/plugins/PluginStore.js +87 -0
  23. package/backend/src/real-time/socketHandler.js +11 -3
  24. package/backend/src/server.js +2 -0
  25. package/backend/temp_migration.sql +0 -0
  26. package/commitlint.config.js +3 -0
  27. package/frontend/dist/assets/index-CHwi1QN9.js +8331 -0
  28. package/frontend/dist/assets/index-DhU2u6V0.css +1 -0
  29. package/frontend/dist/index.html +2 -2
  30. package/frontend/package.json +6 -0
  31. package/package.json +20 -4
  32. package/frontend/dist/assets/index-CIDmlKtb.js +0 -8203
  33. package/frontend/dist/assets/index-DF3i-W3m.css +0 -1
@@ -0,0 +1 @@
1
+ npx --no -- commitlint --edit $1
@@ -0,0 +1 @@
1
+ # npm test
@@ -0,0 +1,17 @@
1
+ {
2
+ "header": "# История версий\n\n",
3
+ "types": [
4
+ { "type": "feat", "section": "✨ Новые возможности" },
5
+ { "type": "fix", "section": "🐛 Исправления" },
6
+ { "type": "perf", "section": "⚡ Производительность" },
7
+ { "type": "refactor", "section": "🛠 Рефакторинг" },
8
+ { "type": "build", "section": "🏗 Сборка" },
9
+ { "type": "chore", "hidden": true },
10
+ { "type": "docs", "hidden": true },
11
+ { "type": "style", "hidden": true },
12
+ { "type": "test", "hidden": true }
13
+ ],
14
+ "writerOpts": {
15
+ "commitPartial": "* {{#if scope}}**{{scope}}:** {{/if}}{{#if subject}}{{subject}}{{/if}} ([{{shortHash}}](https://github.com/blockmineJS/blockmine/commit/{{hash}})) - _{{authorName}}_\n"
16
+ }
17
+ }
package/CHANGELOG.md ADDED
@@ -0,0 +1,36 @@
1
+ # История версий
2
+
3
+
4
+ ## [1.13.0](https://github.com/blockmineJS/blockmine/compare/v1.12.0...v1.13.0) (2025-07-18)
5
+
6
+
7
+ ### ✨ Новые возможности
8
+
9
+ * возможность сбрасывать пароль рут аккаунта ([e23181d](https://github.com/blockmineJS/blockmine/commit/e23181d29dbe730ff882d654b8ec0e80a1f007bc))
10
+
11
+ ## [1.12.0](https://github.com/blockmineJS/blockmine/compare/v1.11.5...v1.12.0) (2025-07-18)
12
+
13
+
14
+ ### ✨ Новые возможности
15
+
16
+ * плагины теперь могу делать свои странички что бы делать какие либо действия в панели ([2664c3c](https://github.com/blockmineJS/blockmine/commit/2664c3c1a02db4e650f8a5be7e96ebbcfe3ab0bb))
17
+ * плагины теперь могуть хранить ингформацию в базе данных ([edb12fd](https://github.com/blockmineJS/blockmine/commit/edb12fd365603bc72c82ad159ffd734cec265dcb))
18
+
19
+
20
+ ### 🛠 Рефакторинг
21
+
22
+ * очередь сообщений. теперь может принимать в себя массив и задержкой отправляет сообщения ([4f193c9](https://github.com/blockmineJS/blockmine/commit/4f193c9c1c76a53ff655e63f520aa83e16c35126))
23
+
24
+ ### [1.11.1](https://github.com/blockmineJS/blockmine/compare/v1.11.0...v1.11.1) (2025-07-17)
25
+
26
+
27
+ ### 🐛 Исправления
28
+
29
+ * test2 ([3ada981](https://github.com/blockmineJS/blockmine/commit/3ada981363de10b9d38cf34f5eb3a00ef527d6b2))
30
+
31
+ ## 1.11.0 (2025-07-17)
32
+
33
+
34
+ ### ✨ Новые возможности
35
+
36
+ * **commands:** Редизайн интерфейса управления командами ([f520049](https://github.com/blockmineJS/blockmine/commit/f520049196dad133ea7957398d512c0334e85917))
package/README.md CHANGED
@@ -33,7 +33,7 @@
33
33
  <td align="center">
34
34
  <p><strong>Управление командами</strong></p>
35
35
  <img src="./image/3.png" alt="Скриншот страницы управления" width="100%">
36
- <em>Настраивайте права, алиасы и кулдауны для каждой команды.</em>
36
+ <em>Настраивайте права алиасы и кулдауны для каждой команды.</em>
37
37
  </td>
38
38
  </tr>
39
39
  </table>
@@ -19,6 +19,7 @@
19
19
  },
20
20
  "dependencies": {
21
21
  "express-validator": "^7.2.1",
22
+
22
23
  "pino": "^9.7.0",
23
24
  "pino-pretty": "^13.0.0"
24
25
  }
@@ -0,0 +1,14 @@
1
+ -- CreateTable
2
+ CREATE TABLE "PluginDataStore" (
3
+ "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
4
+ "pluginName" TEXT NOT NULL,
5
+ "botId" INTEGER NOT NULL,
6
+ "key" TEXT NOT NULL,
7
+ "value" TEXT NOT NULL,
8
+ "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
9
+ "updatedAt" DATETIME NOT NULL,
10
+ CONSTRAINT "PluginDataStore_botId_fkey" FOREIGN KEY ("botId") REFERENCES "Bot" ("id") ON DELETE CASCADE ON UPDATE CASCADE
11
+ );
12
+
13
+ -- CreateIndex
14
+ CREATE UNIQUE INDEX "PluginDataStore_pluginName_botId_key_key" ON "PluginDataStore"("pluginName", "botId", "key");
@@ -38,6 +38,7 @@ model Bot {
38
38
  permissions Permission[]
39
39
  commands Command[]
40
40
  eventGraphs EventGraph[]
41
+ pluginData PluginDataStore[]
41
42
 
42
43
  createdAt DateTime @default(now())
43
44
  updatedAt DateTime @updatedAt
@@ -198,7 +199,21 @@ model PanelRole {
198
199
  name String @unique
199
200
 
200
201
  // Храним права как джсон строку. SQLite не поддерживает массивы.
201
- // Пример: '["bot:create", "bot:delete", "user:manage"]'
202
+ // Пример: '[\"bot:create\", \"bot:delete\", \"user:manage\"]'
202
203
  permissions String @default("[]")
203
204
  users PanelUser[]
204
- }
205
+ }
206
+
207
+ model PluginDataStore {
208
+ id Int @id @default(autoincrement())
209
+ pluginName String
210
+ botId Int
211
+ key String
212
+ value String // в виде json
213
+ createdAt DateTime @default(now())
214
+ updatedAt DateTime @updatedAt
215
+
216
+ bot Bot @relation(fields: [botId], references: [id], onDelete: Cascade)
217
+
218
+ @@unique([pluginName, botId, key])
219
+ }
@@ -1,9 +1,12 @@
1
1
  const express = require('express');
2
2
  const bcrypt = require('bcryptjs');
3
3
  const jwt = require('jsonwebtoken');
4
+ const crypto = require('crypto');
4
5
  const { PrismaClient } = require('@prisma/client');
5
6
  const config = require('../../config');
6
7
  const { authenticate, authorize } = require('../middleware/auth');
8
+ const path = require('path');
9
+ const os = require('os');
7
10
 
8
11
  const router = express.Router();
9
12
  const prisma = new PrismaClient();
@@ -11,6 +14,8 @@ const prisma = new PrismaClient();
11
14
  const JWT_SECRET = config.security.jwtSecret;
12
15
  const JWT_EXPIRES_IN = '7d';
13
16
 
17
+ const activeResetTokens = new Map();
18
+
14
19
  /**
15
20
  * @route GET /api/auth/status
16
21
  * @desc Проверяет, была ли произведена первоначальная настройка (создан админ)
@@ -26,6 +31,16 @@ router.get('/status', async (req, res) => {
26
31
  }
27
32
  });
28
33
 
34
+ /**
35
+ * @route GET /api/auth/config-path
36
+ * @desc Получить путь к конфигурационному файлу
37
+ * @access Public
38
+ */
39
+ router.get('/config-path', (req, res) => {
40
+ const configPath = path.join(os.homedir(), '.blockmine', 'config.json');
41
+ res.json({ configPath });
42
+ });
43
+
29
44
  /**
30
45
  * @route POST /api/auth/setup
31
46
  * @desc Создает первого пользователя с ролью администратора
@@ -100,6 +115,131 @@ router.post('/setup', async (req, res) => {
100
115
  }
101
116
  });
102
117
 
118
+ /**
119
+ * @route POST /api/auth/recovery/verify
120
+ * @desc Проверка кода восстановления
121
+ * @access Public
122
+ */
123
+ router.post('/recovery/verify', async (req, res) => {
124
+ try {
125
+ const { recoveryCode } = req.body;
126
+
127
+ if (!recoveryCode) {
128
+ return res.status(400).json({ error: 'Код восстановления обязателен' });
129
+ }
130
+
131
+ if (recoveryCode !== config.security.adminRecoveryCode) {
132
+ await new Promise(resolve => setTimeout(resolve, 1000));
133
+ return res.status(401).json({ error: 'Неверный код восстановления' });
134
+ }
135
+
136
+ const rootUser = await prisma.panelUser.findFirst({
137
+ orderBy: { id: 'asc' },
138
+ include: { role: true }
139
+ });
140
+
141
+ if (!rootUser) {
142
+ return res.status(404).json({ error: 'В системе нет ни одного пользователя. Выполните первоначальную настройку.' });
143
+ }
144
+
145
+ const tokenId = crypto.randomBytes(16).toString('hex');
146
+ const resetToken = jwt.sign(
147
+ {
148
+ userId: rootUser.id,
149
+ type: 'password-reset',
150
+ tokenId: tokenId,
151
+ timestamp: Date.now()
152
+ },
153
+ JWT_SECRET,
154
+ { expiresIn: '5m' }
155
+ );
156
+
157
+ activeResetTokens.set(tokenId, {
158
+ userId: rootUser.id,
159
+ createdAt: Date.now(),
160
+ used: false
161
+ });
162
+
163
+ setTimeout(() => {
164
+ activeResetTokens.delete(tokenId);
165
+ }, 5 * 60 * 1000);
166
+
167
+ res.json({
168
+ success: true,
169
+ username: rootUser.username,
170
+ resetToken
171
+ });
172
+
173
+ } catch (error) {
174
+ console.error('[Recovery Verify Error]', error);
175
+ res.status(500).json({ error: 'Ошибка при проверке кода восстановления' });
176
+ }
177
+ });
178
+
179
+ /**
180
+ * @route POST /api/auth/recovery/reset
181
+ * @desc Сброс пароля с использованием токена
182
+ * @access Public (с валидным токеном сброса)
183
+ */
184
+ router.post('/recovery/reset', async (req, res) => {
185
+ try {
186
+ const { resetToken, newPassword } = req.body;
187
+
188
+ if (!resetToken || !newPassword) {
189
+ return res.status(400).json({ error: 'Токен и новый пароль обязательны' });
190
+ }
191
+
192
+ if (newPassword.length < 4) {
193
+ return res.status(400).json({ error: 'Пароль должен быть не менее 4 символов' });
194
+ }
195
+
196
+ let decoded;
197
+ try {
198
+ decoded = jwt.verify(resetToken, JWT_SECRET);
199
+ if (decoded.type !== 'password-reset') {
200
+ throw new Error('Invalid token type');
201
+ }
202
+
203
+ const tokenInfo = activeResetTokens.get(decoded.tokenId);
204
+ if (!tokenInfo) {
205
+ throw new Error('Token not found in active tokens');
206
+ }
207
+
208
+ if (tokenInfo.used) {
209
+ throw new Error('Token already used');
210
+ }
211
+
212
+ if (tokenInfo.userId !== decoded.userId) {
213
+ throw new Error('Token userId mismatch');
214
+ }
215
+
216
+ } catch (err) {
217
+ return res.status(401).json({ error: 'Недействительный или истекший токен' });
218
+ }
219
+
220
+ const hashedPassword = await bcrypt.hash(newPassword, 12);
221
+ const updatedUser = await prisma.panelUser.update({
222
+ where: { id: decoded.userId },
223
+ data: { passwordHash: hashedPassword },
224
+ select: { username: true }
225
+ });
226
+
227
+ const tokenInfo = activeResetTokens.get(decoded.tokenId);
228
+ tokenInfo.used = true;
229
+
230
+ activeResetTokens.delete(decoded.tokenId);
231
+
232
+ res.json({
233
+ message: 'Пароль успешно сброшен',
234
+ username: updatedUser.username
235
+ });
236
+
237
+ } catch (error) {
238
+ console.error('[Recovery Reset Error]', error);
239
+ res.status(500).json({ error: 'Ошибка при сбросе пароля' });
240
+ }
241
+ });
242
+
103
243
  /**
104
244
  * @route POST /api/auth/login
105
245
  * @desc Аутентифицирует пользователя и возвращает токен
@@ -1085,4 +1085,180 @@ router.put('/:botId/event-graphs/:graphId', authorize('management:edit'), async
1085
1085
  router.post('/:botId/visual-editor/save', authorize('management:edit'), async (req, res) => {
1086
1086
  });
1087
1087
 
1088
+ router.get('/:botId/ui-extensions', authorize('plugin:list'), async (req, res) => {
1089
+ try {
1090
+ const botId = parseInt(req.params.botId, 10);
1091
+ const enabledPlugins = await prisma.installedPlugin.findMany({
1092
+ where: { botId: botId, isEnabled: true }
1093
+ });
1094
+
1095
+ const extensions = [];
1096
+ for (const plugin of enabledPlugins) {
1097
+ if (plugin.manifest) {
1098
+ try {
1099
+ const manifest = JSON.parse(plugin.manifest);
1100
+ if (manifest.uiExtensions && Array.isArray(manifest.uiExtensions)) {
1101
+ manifest.uiExtensions.forEach(ext => {
1102
+ extensions.push({
1103
+ pluginName: plugin.name,
1104
+ ...ext
1105
+ });
1106
+ });
1107
+ }
1108
+ } catch (e) {
1109
+ console.error(`Ошибка парсинга манифеста для плагина ${plugin.name}:`, e);
1110
+ }
1111
+ }
1112
+ }
1113
+ res.json(extensions);
1114
+ } catch (error) {
1115
+ res.status(500).json({ error: 'Не удалось получить расширения интерфейса' });
1116
+ }
1117
+ });
1118
+
1119
+ router.get('/:botId/plugins/:pluginName/ui-content/:path', authorize('plugin:list'), async (req, res) => {
1120
+ const { botId, pluginName, path: uiPath } = req.params;
1121
+ const numericBotId = parseInt(botId, 10);
1122
+
1123
+ try {
1124
+ const plugin = await prisma.installedPlugin.findFirst({
1125
+ where: { botId: numericBotId, name: pluginName, isEnabled: true }
1126
+ });
1127
+
1128
+ if (!plugin) {
1129
+ return res.status(404).json({ error: `Активный плагин "${pluginName}" не найден для этого бота.` });
1130
+ }
1131
+
1132
+ const manifest = plugin.manifest ? JSON.parse(plugin.manifest) : {};
1133
+ const savedSettings = plugin.settings ? JSON.parse(plugin.settings) : {};
1134
+ const defaultSettings = {};
1135
+
1136
+ if (manifest.settings) {
1137
+ for (const key in manifest.settings) {
1138
+ const config = manifest.settings[key];
1139
+ if (config.type === 'json_file' && config.defaultPath) {
1140
+ const configFilePath = path.join(plugin.path, config.defaultPath);
1141
+ try {
1142
+ const fileContent = await fs.readFile(configFilePath, 'utf-8');
1143
+ defaultSettings[key] = JSON.parse(fileContent);
1144
+ } catch (e) { defaultSettings[key] = {}; }
1145
+ } else {
1146
+ try { defaultSettings[key] = JSON.parse(config.default || 'null'); }
1147
+ catch { defaultSettings[key] = config.default; }
1148
+ }
1149
+ }
1150
+ }
1151
+ const finalSettings = { ...defaultSettings, ...savedSettings };
1152
+
1153
+ const mainFilePath = manifest.main || 'index.js';
1154
+ const pluginEntryPoint = path.join(plugin.path, mainFilePath);
1155
+
1156
+ delete require.cache[require.resolve(pluginEntryPoint)];
1157
+ const pluginModule = require(pluginEntryPoint);
1158
+
1159
+ if (typeof pluginModule.getUiPageContent !== 'function') {
1160
+ return res.status(501).json({ error: `Плагин "${pluginName}" не предоставляет кастомный UI контент.` });
1161
+ }
1162
+
1163
+ const botProcess = botManager.bots.get(numericBotId);
1164
+ const botApi = botProcess ? botProcess.api : null;
1165
+
1166
+ const content = await pluginModule.getUiPageContent({
1167
+ path: uiPath,
1168
+ bot: botApi,
1169
+ botId: numericBotId,
1170
+ settings: finalSettings
1171
+ });
1172
+
1173
+ if (content === null) {
1174
+ return res.status(404).json({ error: `Для пути "${uiPath}" не найдено содержимого в плагине "${pluginName}".` });
1175
+ }
1176
+
1177
+ res.json(content);
1178
+
1179
+ } catch (error) {
1180
+ console.error(`[UI Content] Ошибка при получении контента для плагина "${pluginName}":`, error);
1181
+ res.status(500).json({ error: error.message || 'Внутренняя ошибка сервера.' });
1182
+ }
1183
+ });
1184
+
1185
+
1186
+ router.post('/:botId/plugins/:pluginName/action', authorize('plugin:list'), async (req, res) => {
1187
+ const { botId, pluginName } = req.params;
1188
+ const { actionName, payload } = req.body;
1189
+ const numericBotId = parseInt(botId, 10);
1190
+
1191
+ if (!actionName) {
1192
+ return res.status(400).json({ error: 'Необходимо указать "actionName".' });
1193
+ }
1194
+
1195
+ try {
1196
+ const botProcess = botManager.bots.get(numericBotId);
1197
+
1198
+ if (!botProcess) {
1199
+ return res.status(404).json({ error: 'Бот не найден или не запущен.' });
1200
+ }
1201
+
1202
+ const plugin = await prisma.installedPlugin.findFirst({
1203
+ where: { botId: numericBotId, name: pluginName, isEnabled: true }
1204
+ });
1205
+
1206
+ if (!plugin) {
1207
+ return res.status(404).json({ error: `Активный плагин с таким именем "${pluginName}" не найден.` });
1208
+ }
1209
+
1210
+ const manifest = plugin.manifest ? JSON.parse(plugin.manifest) : {};
1211
+ const savedSettings = plugin.settings ? JSON.parse(plugin.settings) : {};
1212
+ const defaultSettings = {};
1213
+
1214
+ if (manifest.settings) {
1215
+ for (const key in manifest.settings) {
1216
+ const config = manifest.settings[key];
1217
+ if (config.type === 'json_file' && config.defaultPath) {
1218
+ const configFilePath = path.join(plugin.path, config.defaultPath);
1219
+ try {
1220
+ const fileContent = await fs.readFile(configFilePath, 'utf-8');
1221
+ defaultSettings[key] = JSON.parse(fileContent);
1222
+ } catch (e) {
1223
+ console.error(`[Action] Не удалось прочитать defaultPath для ${pluginName}: ${e.message}`);
1224
+ defaultSettings[key] = {};
1225
+ }
1226
+ } else {
1227
+ try {
1228
+ defaultSettings[key] = JSON.parse(config.default || 'null');
1229
+ } catch {
1230
+ defaultSettings[key] = config.default;
1231
+ }
1232
+ }
1233
+ }
1234
+ }
1235
+ const finalSettings = { ...defaultSettings, ...savedSettings };
1236
+
1237
+ const mainFilePath = manifest.main || 'index.js';
1238
+ const pluginPath = path.join(plugin.path, mainFilePath);
1239
+
1240
+ delete require.cache[require.resolve(pluginPath)];
1241
+ const pluginModule = require(pluginPath);
1242
+
1243
+ if (typeof pluginModule.handleAction !== 'function') {
1244
+ return res.status(501).json({ error: `Плагин "${pluginName}" не поддерживает обработку действий.` });
1245
+ }
1246
+
1247
+ const result = await pluginModule.handleAction({
1248
+ botProcess: botProcess,
1249
+ botId: numericBotId,
1250
+ action: actionName,
1251
+ payload: payload,
1252
+ settings: finalSettings
1253
+ });
1254
+
1255
+ res.json({ success: true, message: 'Действие выполнено.', result: result || null });
1256
+
1257
+ } catch (error) {
1258
+ console.error(`Ошибка выполнения действия "${actionName}" для плагина "${pluginName}":`, error);
1259
+ res.status(500).json({ error: error.message || 'Внутренняя ошибка сервера.' });
1260
+ }
1261
+ });
1262
+
1263
+
1088
1264
  module.exports = router;
@@ -0,0 +1,16 @@
1
+ const express = require('express');
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+ const router = express.Router();
5
+
6
+ router.get('/', async (req, res) => {
7
+ try {
8
+ const file = path.resolve(__dirname, '../../../../CHANGELOG.md');
9
+ const data = await fs.promises.readFile(file, 'utf8');
10
+ res.type('text/markdown').send(data);
11
+ } catch (e) {
12
+ res.status(500).json({ error: 'changelog_not_found' });
13
+ }
14
+ });
15
+
16
+ module.exports = router;
@@ -121,7 +121,17 @@ router.put('/:graphId',
121
121
 
122
122
  const parsedGraph = JSON.parse(graphJson);
123
123
  const eventNodes = parsedGraph.nodes.filter(node => node.type.startsWith('event:'));
124
- const eventTypes = eventNodes.map(node => node.type.split(':')[1]);
124
+ const eventTypes = [...new Set(eventNodes.map(node => node.type.split(':')[1]))];
125
+
126
+ const existingGraph = await prisma.eventGraph.findUnique({
127
+ where: { id: parseInt(graphId) },
128
+ include: { triggers: true }
129
+ });
130
+
131
+ const existingEventTypes = existingGraph?.triggers?.map(t => t.eventType) || [];
132
+
133
+ const eventTypesToDelete = existingEventTypes.filter(et => !eventTypes.includes(et));
134
+ const eventTypesToCreate = eventTypes.filter(et => !existingEventTypes.includes(et));
125
135
 
126
136
  dataToUpdate.triggers = {
127
137
  deleteMany: {},
@@ -53,6 +53,17 @@ router.post('/update/:pluginId', authenticate, authorize('plugin:update'), async
53
53
  }
54
54
  });
55
55
 
56
+ router.post('/:id/clear-data', authenticate, authorize('plugin:settings:edit'), async (req, res) => {
57
+ try {
58
+ const pluginId = parseInt(req.params.id);
59
+ await pluginManager.clearPluginData(pluginId);
60
+ res.status(200).json({ message: 'Данные плагина успешно очищены.' });
61
+ } catch (error) {
62
+ console.error(`[API Error] /plugins/:id/clear-data:`, error);
63
+ res.status(500).json({ error: error.message || 'Не удалось очистить данные плагина.' });
64
+ }
65
+ });
66
+
56
67
  router.get('/catalog/:name', async (req, res) => {
57
68
  try {
58
69
  const pluginName = req.params.name;