blockmine 1.25.0 → 1.27.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.
- package/CHANGELOG.md +46 -3
- package/backend/cli.js +1 -1
- package/backend/package.json +2 -2
- package/backend/prisma/migrations/20260328173000_add_plugin_source_ref/migration.sql +2 -0
- package/backend/prisma/migrations/migration_lock.toml +2 -2
- package/backend/prisma/schema.prisma +2 -0
- package/backend/src/api/routes/apiKeys.js +8 -0
- package/backend/src/api/routes/bots.js +258 -9
- package/backend/src/api/routes/eventGraphs.js +151 -1
- package/backend/src/api/routes/health.js +38 -0
- package/backend/src/api/routes/nodeRegistry.js +63 -0
- package/backend/src/api/routes/plugins.js +254 -29
- package/backend/src/container.js +11 -8
- package/backend/src/core/BotCommandLoader.js +161 -0
- package/backend/src/core/BotConnection.js +125 -0
- package/backend/src/core/BotEventHandlers.js +234 -0
- package/backend/src/core/BotIPCHandler.js +445 -0
- package/backend/src/core/BotManager.js +15 -7
- package/backend/src/core/BotProcess.js +75 -142
- package/backend/src/core/EventGraphManager.js +7 -3
- package/backend/src/core/GraphDebugHandler.js +229 -0
- package/backend/src/core/GraphDebugIPC.js +117 -0
- package/backend/src/core/GraphExecutionEngine.js +545 -978
- package/backend/src/core/GraphTraversal.js +80 -0
- package/backend/src/core/GraphValidation.js +73 -0
- package/backend/src/core/NodeDefinition.js +138 -0
- package/backend/src/core/NodeRegistry.js +153 -141
- package/backend/src/core/PluginManager.js +272 -31
- package/backend/src/core/RewindSignal.js +9 -0
- package/backend/src/core/config/ConfigValidator.js +72 -0
- package/backend/src/core/config/FeatureFlags.js +52 -0
- package/backend/src/core/config/__tests__/ConfigValidator.test.js +232 -0
- package/backend/src/core/domain/entities/Bot.js +39 -0
- package/backend/src/core/domain/entities/Command.js +41 -0
- package/backend/src/core/domain/entities/EventGraph.js +39 -0
- package/backend/src/core/domain/entities/Plugin.js +45 -0
- package/backend/src/core/domain/entities/User.js +40 -0
- package/backend/src/core/domain/services/DependencyResolver.js +168 -0
- package/backend/src/core/domain/services/GraphValidator.js +117 -0
- package/backend/src/core/domain/services/PermissionChecker.js +34 -0
- package/backend/src/core/domain/services/__tests__/DependencyResolver.test.js +126 -0
- package/backend/src/core/domain/valueObjects/BotConfig.js +27 -0
- package/backend/src/core/domain/valueObjects/DependencyGraph.js +86 -0
- package/backend/src/core/domain/valueObjects/PluginManifest.js +36 -0
- package/backend/src/core/errors/BaseError.js +29 -0
- package/backend/src/core/errors/ErrorHandler.js +81 -0
- package/backend/src/core/errors/__tests__/ErrorHandler.test.js +188 -0
- package/backend/src/core/errors/index.js +68 -0
- package/backend/src/core/infrastructure/BatchingUtility.js +66 -0
- package/backend/src/core/infrastructure/CircuitBreaker.js +103 -0
- package/backend/src/core/infrastructure/ConnectionPool.js +81 -0
- package/backend/src/core/infrastructure/RateLimiter.js +64 -0
- package/backend/src/core/infrastructure/__tests__/BatchingUtility.test.js +86 -0
- package/backend/src/core/infrastructure/__tests__/CircuitBreaker.test.js +156 -0
- package/backend/src/core/infrastructure/__tests__/ConnectionPool.test.js +146 -0
- package/backend/src/core/infrastructure/__tests__/RateLimiter.test.js +171 -0
- package/backend/src/core/ipc/botApiFactory.js +72 -0
- package/backend/src/core/ipc/ipcMessageTypes.js +115 -0
- package/backend/src/core/logging/AuditLogger.js +61 -0
- package/backend/src/core/logging/StructuredLogger.js +80 -0
- package/backend/src/core/logging/__tests__/StructuredLogger.test.js +213 -0
- package/backend/src/core/logging/index.js +7 -0
- package/backend/src/core/metrics/MetricsCollector.js +104 -0
- package/backend/src/core/metrics/__tests__/MetricsCollector.test.js +131 -0
- package/backend/src/core/node-registries/actionsNodes.js +191 -0
- package/backend/src/core/node-registries/arraysNodes.js +152 -0
- package/backend/src/core/node-registries/botNodes.js +48 -0
- package/backend/src/core/node-registries/containerNodes.js +141 -0
- package/backend/src/core/node-registries/dataNodes.js +284 -0
- package/backend/src/core/node-registries/debugNodes.js +23 -0
- package/backend/src/core/node-registries/eventsNodes.js +223 -0
- package/backend/src/core/node-registries/flowNodes.js +151 -0
- package/backend/src/core/node-registries/furnaceNodes.js +123 -0
- package/backend/src/core/node-registries/index.js +108 -0
- package/backend/src/core/node-registries/inventory.js +102 -106
- package/backend/src/core/node-registries/logicNodes.js +54 -0
- package/backend/src/core/node-registries/mathNodes.js +38 -0
- package/backend/src/core/node-registries/navigationNodes.js +109 -0
- package/backend/src/core/node-registries/objectsNodes.js +90 -0
- package/backend/src/core/node-registries/stringsNodes.js +165 -0
- package/backend/src/core/node-registries/timeNodes.js +105 -0
- package/backend/src/core/node-registries/typeNodes.js +22 -0
- package/backend/src/core/node-registries/usersNodes.js +126 -0
- package/backend/src/core/nodes/arrays/shuffle.js +14 -0
- package/backend/src/core/nodes/bot/get_name.js +8 -0
- package/backend/src/core/nodes/bot/stop_bot.js +5 -0
- package/backend/src/core/nodes/container/open.js +101 -111
- package/backend/src/core/nodes/data/store_read.js +26 -0
- package/backend/src/core/nodes/data/store_write.js +23 -0
- package/backend/src/core/nodes/event/call_event.js +31 -0
- package/backend/src/core/nodes/event/custom_event.js +8 -0
- package/backend/src/core/nodes/flow/timer.js +35 -0
- package/backend/src/core/nodes/inventory/drop.js +73 -65
- package/backend/src/core/nodes/inventory/equip.js +54 -45
- package/backend/src/core/nodes/inventory/select_slot.js +48 -46
- package/backend/src/core/nodes/navigation/follow.js +54 -51
- package/backend/src/core/nodes/navigation/go_to.js +41 -53
- package/backend/src/core/nodes/navigation/go_to_entity.js +65 -69
- package/backend/src/core/nodes/navigation/go_to_player.js +65 -70
- package/backend/src/core/nodes/navigation/stop.js +17 -26
- package/backend/src/core/nodes/users/add_to_group.js +24 -0
- package/backend/src/core/nodes/users/check_permission.js +26 -0
- package/backend/src/core/nodes/users/remove_from_group.js +24 -0
- package/backend/src/core/services/BotIPCMessageRouter.js +337 -0
- package/backend/src/core/services/BotLifecycleService.js +41 -632
- package/backend/src/core/services/CacheManager.js +83 -23
- package/backend/src/core/services/CrashRestartManager.js +42 -0
- package/backend/src/core/services/DebugSessionManager.js +114 -12
- package/backend/src/core/services/EventGraphService.js +69 -0
- package/backend/src/core/services/MinecraftBotManager.js +9 -1
- package/backend/src/core/services/PluginManagementService.js +84 -0
- package/backend/src/core/services/TestModeContext.js +65 -0
- package/backend/src/core/services/__tests__/CacheManager.test.js +168 -0
- package/backend/src/core/services.js +1 -11
- package/backend/src/core/validation/InputValidator.js +167 -0
- package/backend/src/core/validation/__tests__/InputValidator.test.js +296 -0
- package/backend/src/real-time/botApi/index.js +1 -1
- package/backend/src/real-time/socketHandler.js +26 -0
- package/backend/src/server.js +10 -5
- package/frontend/dist/assets/{browser-ponyfill-DN7pwmHT.js → browser-ponyfill-D8y0Ty7C.js} +1 -1
- package/frontend/dist/assets/index-CFJLS0dk.css +32 -0
- package/frontend/dist/assets/{index-LSy71uwm.js → index-D91UGNMG.js} +1880 -1881
- package/frontend/dist/index.html +2 -2
- package/frontend/dist/locales/en/bots.json +4 -1
- package/frontend/dist/locales/en/common.json +7 -1
- package/frontend/dist/locales/en/login.json +2 -0
- package/frontend/dist/locales/en/management.json +79 -1
- package/frontend/dist/locales/en/nodes.json +59 -4
- package/frontend/dist/locales/en/plugin-detail.json +24 -4
- package/frontend/dist/locales/en/plugins.json +226 -7
- package/frontend/dist/locales/en/setup.json +2 -0
- package/frontend/dist/locales/en/sidebar.json +171 -3
- package/frontend/dist/locales/en/visual-editor.json +230 -31
- package/frontend/dist/locales/ru/bots.json +4 -1
- package/frontend/dist/locales/ru/login.json +2 -0
- package/frontend/dist/locales/ru/management.json +79 -1
- package/frontend/dist/locales/ru/minecraft-viewer.json +3 -0
- package/frontend/dist/locales/ru/nodes.json +105 -51
- package/frontend/dist/locales/ru/plugins.json +103 -4
- package/frontend/dist/locales/ru/setup.json +2 -0
- package/frontend/dist/locales/ru/sidebar.json +171 -3
- package/frontend/dist/locales/ru/visual-editor.json +232 -33
- package/frontend/package.json +2 -0
- package/nul +12 -0
- package/package.json +3 -3
- package/backend/package-lock.json +0 -6801
- package/backend/src/core/node-registries/actions.js +0 -202
- package/backend/src/core/node-registries/arrays.js +0 -155
- package/backend/src/core/node-registries/bot.js +0 -23
- package/backend/src/core/node-registries/container.js +0 -162
- package/backend/src/core/node-registries/data.js +0 -290
- package/backend/src/core/node-registries/debug.js +0 -26
- package/backend/src/core/node-registries/events.js +0 -201
- package/backend/src/core/node-registries/flow.js +0 -139
- package/backend/src/core/node-registries/furnace.js +0 -143
- package/backend/src/core/node-registries/logic.js +0 -62
- package/backend/src/core/node-registries/math.js +0 -42
- package/backend/src/core/node-registries/navigation.js +0 -111
- package/backend/src/core/node-registries/objects.js +0 -98
- package/backend/src/core/node-registries/strings.js +0 -187
- package/backend/src/core/node-registries/time.js +0 -113
- package/backend/src/core/node-registries/type.js +0 -25
- package/backend/src/core/node-registries/users.js +0 -79
- package/frontend/dist/assets/index-SfhKxI4-.css +0 -32
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
const CacheManager = require('../CacheManager');
|
|
2
|
+
|
|
3
|
+
describe('CacheManager', () => {
|
|
4
|
+
let cache;
|
|
5
|
+
|
|
6
|
+
beforeEach(() => {
|
|
7
|
+
cache = new CacheManager();
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
describe('getMetrics', () => {
|
|
11
|
+
it('returns zero metrics on init', () => {
|
|
12
|
+
const metrics = cache.getMetrics();
|
|
13
|
+
expect(metrics.tokenCache).toEqual({ hits: 0, misses: 0, hitRate: 0 });
|
|
14
|
+
expect(metrics.playerListCache).toEqual({ hits: 0, misses: 0, hitRate: 0 });
|
|
15
|
+
expect(metrics.botConfigsCache).toEqual({ hits: 0, misses: 0, hitRate: 0 });
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('tracks tokenCache hits and misses', () => {
|
|
19
|
+
cache.setToken('tok1', { user: 'alice' });
|
|
20
|
+
cache.getToken('tok1');
|
|
21
|
+
cache.getToken('tok-missing');
|
|
22
|
+
|
|
23
|
+
const { tokenCache } = cache.getMetrics();
|
|
24
|
+
expect(tokenCache.hits).toBe(1);
|
|
25
|
+
expect(tokenCache.misses).toBe(1);
|
|
26
|
+
expect(tokenCache.hitRate).toBe(0.5);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('tracks playerListCache hits and misses', () => {
|
|
30
|
+
cache.setPlayerList(1, ['player1']);
|
|
31
|
+
cache.getPlayerList(1);
|
|
32
|
+
cache.getPlayerList(999);
|
|
33
|
+
|
|
34
|
+
const { playerListCache } = cache.getMetrics();
|
|
35
|
+
expect(playerListCache.hits).toBe(1);
|
|
36
|
+
expect(playerListCache.misses).toBe(1);
|
|
37
|
+
expect(playerListCache.hitRate).toBe(0.5);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('tracks botConfigsCache hits and misses', () => {
|
|
41
|
+
cache.setBotConfig(1, { commands: new Map() });
|
|
42
|
+
cache.getBotConfig(1);
|
|
43
|
+
cache.getBotConfig(999);
|
|
44
|
+
|
|
45
|
+
const { botConfigsCache } = cache.getMetrics();
|
|
46
|
+
expect(botConfigsCache.hits).toBe(1);
|
|
47
|
+
expect(botConfigsCache.misses).toBe(1);
|
|
48
|
+
expect(botConfigsCache.hitRate).toBe(0.5);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('computes hitRate as 1 when all are hits', () => {
|
|
52
|
+
cache.setToken('t', 'v');
|
|
53
|
+
cache.getToken('t');
|
|
54
|
+
cache.getToken('t');
|
|
55
|
+
|
|
56
|
+
const { tokenCache } = cache.getMetrics();
|
|
57
|
+
expect(tokenCache.hitRate).toBe(1);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('computes hitRate as 0 when all are misses', () => {
|
|
61
|
+
cache.getToken('nope');
|
|
62
|
+
cache.getToken('nope2');
|
|
63
|
+
|
|
64
|
+
const { tokenCache } = cache.getMetrics();
|
|
65
|
+
expect(tokenCache.hitRate).toBe(0);
|
|
66
|
+
expect(tokenCache.hits).toBe(0);
|
|
67
|
+
expect(tokenCache.misses).toBe(2);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe('resetMetrics', () => {
|
|
72
|
+
it('resets all counters to zero', () => {
|
|
73
|
+
cache.setToken('t', 'v');
|
|
74
|
+
cache.getToken('t');
|
|
75
|
+
cache.getToken('missing');
|
|
76
|
+
cache.setPlayerList(1, []);
|
|
77
|
+
cache.getPlayerList(1);
|
|
78
|
+
|
|
79
|
+
cache.resetMetrics();
|
|
80
|
+
|
|
81
|
+
const metrics = cache.getMetrics();
|
|
82
|
+
expect(metrics.tokenCache).toEqual({ hits: 0, misses: 0, hitRate: 0 });
|
|
83
|
+
expect(metrics.playerListCache).toEqual({ hits: 0, misses: 0, hitRate: 0 });
|
|
84
|
+
expect(metrics.botConfigsCache).toEqual({ hits: 0, misses: 0, hitRate: 0 });
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
describe('checkMemoryUsage', () => {
|
|
89
|
+
it('returns heapUsed, threshold, and exceeded fields', () => {
|
|
90
|
+
const result = cache.checkMemoryUsage();
|
|
91
|
+
expect(typeof result.heapUsed).toBe('number');
|
|
92
|
+
expect(typeof result.threshold).toBe('number');
|
|
93
|
+
expect(typeof result.exceeded).toBe('boolean');
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('does not clear playerListCache when under threshold', () => {
|
|
97
|
+
cache.memoryThreshold = Number.MAX_SAFE_INTEGER;
|
|
98
|
+
cache.setPlayerList(1, ['player1']);
|
|
99
|
+
|
|
100
|
+
const result = cache.checkMemoryUsage();
|
|
101
|
+
|
|
102
|
+
expect(result.exceeded).toBe(false);
|
|
103
|
+
expect(cache.getPlayerList(1)).toEqual(['player1']);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('clears playerListCache when threshold is exceeded', () => {
|
|
107
|
+
cache.memoryThreshold = 1;
|
|
108
|
+
cache.setPlayerList(1, ['player1']);
|
|
109
|
+
cache.setPlayerList(2, ['player2']);
|
|
110
|
+
|
|
111
|
+
const result = cache.checkMemoryUsage();
|
|
112
|
+
|
|
113
|
+
expect(result.exceeded).toBe(true);
|
|
114
|
+
expect(cache.playerListCache.size).toBe(0);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('logs a warning when threshold is exceeded', () => {
|
|
118
|
+
const logger = { warn: jest.fn() };
|
|
119
|
+
cache = new CacheManager({ logger });
|
|
120
|
+
cache.memoryThreshold = 1;
|
|
121
|
+
|
|
122
|
+
cache.checkMemoryUsage();
|
|
123
|
+
|
|
124
|
+
expect(logger.warn).toHaveBeenCalledWith(
|
|
125
|
+
expect.objectContaining({ threshold: 1 }),
|
|
126
|
+
expect.any(String)
|
|
127
|
+
);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('does not log when no logger is provided', () => {
|
|
131
|
+
cache.memoryThreshold = 1;
|
|
132
|
+
expect(() => cache.checkMemoryUsage()).not.toThrow();
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
describe('backward compatibility', () => {
|
|
137
|
+
it('getToken/setToken/deleteToken work as before', () => {
|
|
138
|
+
cache.setToken('abc', { id: 1 });
|
|
139
|
+
expect(cache.getToken('abc')).toEqual({ id: 1 });
|
|
140
|
+
cache.deleteToken('abc');
|
|
141
|
+
expect(cache.getToken('abc')).toBeUndefined();
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('getPlayerList/setPlayerList work as before', () => {
|
|
145
|
+
cache.setPlayerList(42, ['a', 'b']);
|
|
146
|
+
expect(cache.getPlayerList(42)).toEqual(['a', 'b']);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('getBotConfig/setBotConfig/deleteBotConfig work as before', () => {
|
|
150
|
+
cache.setBotConfig(7, { commands: new Map() });
|
|
151
|
+
expect(cache.getBotConfig(7)).toBeDefined();
|
|
152
|
+
cache.deleteBotConfig(7);
|
|
153
|
+
expect(cache.getBotConfig(7)).toBeUndefined();
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('clearBotCache removes both playerList and botConfig', () => {
|
|
157
|
+
cache.setPlayerList(5, ['x']);
|
|
158
|
+
cache.setBotConfig(5, { commands: new Map() });
|
|
159
|
+
cache.clearBotCache(5);
|
|
160
|
+
expect(cache.getPlayerList(5)).toBeUndefined();
|
|
161
|
+
expect(cache.getBotConfig(5)).toBeUndefined();
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('botConfigsCache remains a Map', () => {
|
|
165
|
+
expect(cache.botConfigsCache).toBeInstanceOf(Map);
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
});
|
|
@@ -1,22 +1,12 @@
|
|
|
1
|
-
const { asValue } = require('awilix');
|
|
2
1
|
const configureContainer = require('../container');
|
|
3
2
|
|
|
4
3
|
const container = configureContainer();
|
|
5
4
|
|
|
6
|
-
const
|
|
7
|
-
const botManager = new BotManager(container.cradle);
|
|
8
|
-
|
|
9
|
-
// Регистрируем botManager в контейнере для PluginManager
|
|
10
|
-
container.register({
|
|
11
|
-
botManager: asValue(botManager)
|
|
12
|
-
});
|
|
13
|
-
|
|
5
|
+
const botManager = container.resolve('botManager');
|
|
14
6
|
const pluginManager = container.resolve('pluginManager');
|
|
15
7
|
const eventGraphManager = container.resolve('eventGraphManager');
|
|
16
8
|
|
|
17
|
-
// EventGraphManager требует botManager через setter из-за циклической зависимости
|
|
18
9
|
eventGraphManager.setBotManager(botManager);
|
|
19
|
-
|
|
20
10
|
botManager.initialize();
|
|
21
11
|
|
|
22
12
|
module.exports = {
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
const SEMVER_PATTERN = /^\d+\.\d+\.\d+(-[\w.]+)?(\+[\w.]+)?$/;
|
|
2
|
+
|
|
3
|
+
const DANGEROUS_PATTERNS = [
|
|
4
|
+
/<script\b[^>]*>[\s\S]*?<\/script(?:\s+[^>]*)?\s*>/gi,
|
|
5
|
+
/<[^>]+on\w+\s*=\s*["'][^"']*["'][^>]*>/gi,
|
|
6
|
+
/javascript\s*:/gi,
|
|
7
|
+
/vbscript\s*:/gi,
|
|
8
|
+
/union\s+select/gi,
|
|
9
|
+
/drop\s+table/gi,
|
|
10
|
+
/insert\s+into/gi,
|
|
11
|
+
/delete\s+from/gi,
|
|
12
|
+
/update\s+\w+\s+set/gi,
|
|
13
|
+
/exec\s*\(/gi,
|
|
14
|
+
/xp_cmdshell/gi,
|
|
15
|
+
/\.\.[/\\]/g,
|
|
16
|
+
/[/\\]\.\./g,
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
function sanitizeString(value) {
|
|
20
|
+
if (typeof value !== 'string') return '';
|
|
21
|
+
let result = value;
|
|
22
|
+
for (const pattern of DANGEROUS_PATTERNS) {
|
|
23
|
+
result = result.replace(pattern, '');
|
|
24
|
+
}
|
|
25
|
+
return result;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function validateString(value, options = {}) {
|
|
29
|
+
const { minLength, maxLength, pattern, required } = options;
|
|
30
|
+
|
|
31
|
+
if (value === undefined || value === null || value === '') {
|
|
32
|
+
if (required) {
|
|
33
|
+
return { valid: false, value: '', error: 'validation.string.required' };
|
|
34
|
+
}
|
|
35
|
+
return { valid: true, value: '' };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (typeof value !== 'string') {
|
|
39
|
+
return { valid: false, value: '', error: 'validation.string.type' };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const sanitized = sanitizeString(value);
|
|
43
|
+
|
|
44
|
+
if (minLength !== undefined && sanitized.length < minLength) {
|
|
45
|
+
return { valid: false, value: sanitized, error: 'validation.string.minLength' };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (maxLength !== undefined && sanitized.length > maxLength) {
|
|
49
|
+
return { valid: false, value: sanitized, error: 'validation.string.maxLength' };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (pattern !== undefined && !pattern.test(sanitized)) {
|
|
53
|
+
return { valid: false, value: sanitized, error: 'validation.string.pattern' };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return { valid: true, value: sanitized };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function validateInteger(value, options = {}) {
|
|
60
|
+
const { min, max, required } = options;
|
|
61
|
+
|
|
62
|
+
if (value === undefined || value === null || value === '') {
|
|
63
|
+
if (required) {
|
|
64
|
+
return { valid: false, value: 0, error: 'validation.integer.required' };
|
|
65
|
+
}
|
|
66
|
+
return { valid: true, value: 0 };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const parsed = Number(value);
|
|
70
|
+
|
|
71
|
+
if (!Number.isInteger(parsed)) {
|
|
72
|
+
return { valid: false, value: 0, error: 'validation.integer.type' };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (min !== undefined && parsed < min) {
|
|
76
|
+
return { valid: false, value: parsed, error: 'validation.integer.min' };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (max !== undefined && parsed > max) {
|
|
80
|
+
return { valid: false, value: parsed, error: 'validation.integer.max' };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return { valid: true, value: parsed };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function validatePluginManifest(manifest) {
|
|
87
|
+
const errors = [];
|
|
88
|
+
|
|
89
|
+
if (!manifest || typeof manifest !== 'object') {
|
|
90
|
+
return { valid: false, errors: ['validation.manifest.type'] };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (!manifest.name || typeof manifest.name !== 'string' || manifest.name.trim() === '') {
|
|
94
|
+
errors.push('validation.manifest.name.required');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (!manifest.version || typeof manifest.version !== 'string') {
|
|
98
|
+
errors.push('validation.manifest.version.required');
|
|
99
|
+
} else if (!SEMVER_PATTERN.test(manifest.version)) {
|
|
100
|
+
errors.push('validation.manifest.version.semver');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (!manifest.main || typeof manifest.main !== 'string' || manifest.main.trim() === '') {
|
|
104
|
+
errors.push('validation.manifest.main.required');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (manifest.description !== undefined && typeof manifest.description !== 'string') {
|
|
108
|
+
errors.push('validation.manifest.description.type');
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (manifest.dependencies !== undefined && (typeof manifest.dependencies !== 'object' || Array.isArray(manifest.dependencies))) {
|
|
112
|
+
errors.push('validation.manifest.dependencies.type');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (manifest.settings !== undefined && (typeof manifest.settings !== 'object' || Array.isArray(manifest.settings))) {
|
|
116
|
+
errors.push('validation.manifest.settings.type');
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return { valid: errors.length === 0, errors };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function validateBotConfig(config) {
|
|
123
|
+
const errors = [];
|
|
124
|
+
|
|
125
|
+
if (!config || typeof config !== 'object') {
|
|
126
|
+
return { valid: false, errors: ['validation.botConfig.type'] };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const hostResult = validateString(config.host, { required: true, minLength: 1 });
|
|
130
|
+
if (!hostResult.valid) {
|
|
131
|
+
errors.push('validation.botConfig.host.required');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const portResult = validateInteger(config.port, { required: true, min: 1, max: 65535 });
|
|
135
|
+
if (!portResult.valid) {
|
|
136
|
+
errors.push(portResult.error === 'validation.integer.required'
|
|
137
|
+
? 'validation.botConfig.port.required'
|
|
138
|
+
: 'validation.botConfig.port.range');
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const usernameResult = validateString(config.username, { required: true, minLength: 1 });
|
|
142
|
+
if (!usernameResult.valid) {
|
|
143
|
+
errors.push('validation.botConfig.username.required');
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (config.password !== undefined && typeof config.password !== 'string') {
|
|
147
|
+
errors.push('validation.botConfig.password.type');
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (config.version !== undefined && typeof config.version !== 'string') {
|
|
151
|
+
errors.push('validation.botConfig.version.type');
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (config.proxy !== undefined && typeof config.proxy !== 'object') {
|
|
155
|
+
errors.push('validation.botConfig.proxy.type');
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return { valid: errors.length === 0, errors };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
module.exports = {
|
|
162
|
+
validateString,
|
|
163
|
+
validateInteger,
|
|
164
|
+
validatePluginManifest,
|
|
165
|
+
validateBotConfig,
|
|
166
|
+
sanitizeString,
|
|
167
|
+
};
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
const {
|
|
2
|
+
validateString,
|
|
3
|
+
validateInteger,
|
|
4
|
+
validatePluginManifest,
|
|
5
|
+
validateBotConfig,
|
|
6
|
+
sanitizeString,
|
|
7
|
+
} = require('../InputValidator');
|
|
8
|
+
|
|
9
|
+
describe('InputValidator', () => {
|
|
10
|
+
describe('sanitizeString', () => {
|
|
11
|
+
it('removes script tags', () => {
|
|
12
|
+
const result = sanitizeString('<script>alert("xss")</script>hello');
|
|
13
|
+
expect(result).not.toContain('<script>');
|
|
14
|
+
expect(result).toContain('hello');
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('removes SQL injection patterns', () => {
|
|
18
|
+
const result = sanitizeString("1 UNION SELECT * FROM users");
|
|
19
|
+
expect(result.toLowerCase()).not.toContain('union select');
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('removes path traversal patterns', () => {
|
|
23
|
+
const result = sanitizeString('../../etc/passwd');
|
|
24
|
+
expect(result).not.toContain('../');
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('removes javascript: protocol', () => {
|
|
28
|
+
const result = sanitizeString('javascript:alert(1)');
|
|
29
|
+
expect(result.toLowerCase()).not.toContain('javascript:');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('returns empty string for non-string input', () => {
|
|
33
|
+
expect(sanitizeString(null)).toBe('');
|
|
34
|
+
expect(sanitizeString(undefined)).toBe('');
|
|
35
|
+
expect(sanitizeString(123)).toBe('');
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('returns clean string unchanged', () => {
|
|
39
|
+
expect(sanitizeString('hello world')).toBe('hello world');
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe('validateString', () => {
|
|
44
|
+
it('returns valid for a normal string', () => {
|
|
45
|
+
const result = validateString('hello');
|
|
46
|
+
expect(result.valid).toBe(true);
|
|
47
|
+
expect(result.value).toBe('hello');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('returns error when required and empty', () => {
|
|
51
|
+
const result = validateString('', { required: true });
|
|
52
|
+
expect(result.valid).toBe(false);
|
|
53
|
+
expect(result.error).toBe('validation.string.required');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('returns valid when not required and empty', () => {
|
|
57
|
+
const result = validateString('');
|
|
58
|
+
expect(result.valid).toBe(true);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('returns error when below minLength', () => {
|
|
62
|
+
const result = validateString('ab', { minLength: 5 });
|
|
63
|
+
expect(result.valid).toBe(false);
|
|
64
|
+
expect(result.error).toBe('validation.string.minLength');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('returns error when above maxLength', () => {
|
|
68
|
+
const result = validateString('hello world', { maxLength: 5 });
|
|
69
|
+
expect(result.valid).toBe(false);
|
|
70
|
+
expect(result.error).toBe('validation.string.maxLength');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('returns error when pattern does not match', () => {
|
|
74
|
+
const result = validateString('abc123', { pattern: /^\d+$/ });
|
|
75
|
+
expect(result.valid).toBe(false);
|
|
76
|
+
expect(result.error).toBe('validation.string.pattern');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('returns valid when pattern matches', () => {
|
|
80
|
+
const result = validateString('12345', { pattern: /^\d+$/ });
|
|
81
|
+
expect(result.valid).toBe(true);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('sanitizes dangerous content', () => {
|
|
85
|
+
const result = validateString('<script>alert(1)</script>safe');
|
|
86
|
+
expect(result.valid).toBe(true);
|
|
87
|
+
expect(result.value).not.toContain('<script>');
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('returns error for non-string type', () => {
|
|
91
|
+
const result = validateString(42);
|
|
92
|
+
expect(result.valid).toBe(false);
|
|
93
|
+
expect(result.error).toBe('validation.string.type');
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
describe('validateInteger', () => {
|
|
98
|
+
it('returns valid for a normal integer', () => {
|
|
99
|
+
const result = validateInteger(5);
|
|
100
|
+
expect(result.valid).toBe(true);
|
|
101
|
+
expect(result.value).toBe(5);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('parses string integers', () => {
|
|
105
|
+
const result = validateInteger('42');
|
|
106
|
+
expect(result.valid).toBe(true);
|
|
107
|
+
expect(result.value).toBe(42);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('returns error when required and missing', () => {
|
|
111
|
+
const result = validateInteger(undefined, { required: true });
|
|
112
|
+
expect(result.valid).toBe(false);
|
|
113
|
+
expect(result.error).toBe('validation.integer.required');
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('returns valid when not required and missing', () => {
|
|
117
|
+
const result = validateInteger(undefined);
|
|
118
|
+
expect(result.valid).toBe(true);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('returns error for float', () => {
|
|
122
|
+
const result = validateInteger(3.14);
|
|
123
|
+
expect(result.valid).toBe(false);
|
|
124
|
+
expect(result.error).toBe('validation.integer.type');
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('returns error when below min', () => {
|
|
128
|
+
const result = validateInteger(0, { min: 1 });
|
|
129
|
+
expect(result.valid).toBe(false);
|
|
130
|
+
expect(result.error).toBe('validation.integer.min');
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('returns error when above max', () => {
|
|
134
|
+
const result = validateInteger(100, { max: 50 });
|
|
135
|
+
expect(result.valid).toBe(false);
|
|
136
|
+
expect(result.error).toBe('validation.integer.max');
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('returns valid at boundary values', () => {
|
|
140
|
+
expect(validateInteger(1, { min: 1, max: 65535 }).valid).toBe(true);
|
|
141
|
+
expect(validateInteger(65535, { min: 1, max: 65535 }).valid).toBe(true);
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
describe('validatePluginManifest', () => {
|
|
146
|
+
const validManifest = {
|
|
147
|
+
name: 'my-plugin',
|
|
148
|
+
version: '1.0.0',
|
|
149
|
+
main: 'index.js',
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
it('returns valid for a correct manifest', () => {
|
|
153
|
+
const result = validatePluginManifest(validManifest);
|
|
154
|
+
expect(result.valid).toBe(true);
|
|
155
|
+
expect(result.errors).toHaveLength(0);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('returns error when name is missing', () => {
|
|
159
|
+
const result = validatePluginManifest({ ...validManifest, name: undefined });
|
|
160
|
+
expect(result.valid).toBe(false);
|
|
161
|
+
expect(result.errors).toContain('validation.manifest.name.required');
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('returns error when version is not semver', () => {
|
|
165
|
+
const result = validatePluginManifest({ ...validManifest, version: 'not-semver' });
|
|
166
|
+
expect(result.valid).toBe(false);
|
|
167
|
+
expect(result.errors).toContain('validation.manifest.version.semver');
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('returns error when main is missing', () => {
|
|
171
|
+
const result = validatePluginManifest({ ...validManifest, main: '' });
|
|
172
|
+
expect(result.valid).toBe(false);
|
|
173
|
+
expect(result.errors).toContain('validation.manifest.main.required');
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('accepts optional description', () => {
|
|
177
|
+
const result = validatePluginManifest({ ...validManifest, description: 'A plugin' });
|
|
178
|
+
expect(result.valid).toBe(true);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('returns error when description is not a string', () => {
|
|
182
|
+
const result = validatePluginManifest({ ...validManifest, description: 123 });
|
|
183
|
+
expect(result.valid).toBe(false);
|
|
184
|
+
expect(result.errors).toContain('validation.manifest.description.type');
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('accepts optional dependencies as object', () => {
|
|
188
|
+
const result = validatePluginManifest({ ...validManifest, dependencies: { 'other-plugin': '^1.0.0' } });
|
|
189
|
+
expect(result.valid).toBe(true);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('returns error when dependencies is an array', () => {
|
|
193
|
+
const result = validatePluginManifest({ ...validManifest, dependencies: [] });
|
|
194
|
+
expect(result.valid).toBe(false);
|
|
195
|
+
expect(result.errors).toContain('validation.manifest.dependencies.type');
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('accepts optional settings as object', () => {
|
|
199
|
+
const result = validatePluginManifest({ ...validManifest, settings: { key: 'value' } });
|
|
200
|
+
expect(result.valid).toBe(true);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it('returns error for non-object input', () => {
|
|
204
|
+
const result = validatePluginManifest(null);
|
|
205
|
+
expect(result.valid).toBe(false);
|
|
206
|
+
expect(result.errors).toContain('validation.manifest.type');
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it('accepts semver with prerelease', () => {
|
|
210
|
+
const result = validatePluginManifest({ ...validManifest, version: '1.0.0-beta.1' });
|
|
211
|
+
expect(result.valid).toBe(true);
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
describe('validateBotConfig', () => {
|
|
216
|
+
const validConfig = {
|
|
217
|
+
host: 'localhost',
|
|
218
|
+
port: 25565,
|
|
219
|
+
username: 'testbot',
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
it('returns valid for a correct config', () => {
|
|
223
|
+
const result = validateBotConfig(validConfig);
|
|
224
|
+
expect(result.valid).toBe(true);
|
|
225
|
+
expect(result.errors).toHaveLength(0);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it('returns error when host is missing', () => {
|
|
229
|
+
const result = validateBotConfig({ ...validConfig, host: '' });
|
|
230
|
+
expect(result.valid).toBe(false);
|
|
231
|
+
expect(result.errors).toContain('validation.botConfig.host.required');
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it('returns error when port is missing', () => {
|
|
235
|
+
const result = validateBotConfig({ ...validConfig, port: undefined });
|
|
236
|
+
expect(result.valid).toBe(false);
|
|
237
|
+
expect(result.errors).toContain('validation.botConfig.port.required');
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it('returns error when port is out of range', () => {
|
|
241
|
+
const result = validateBotConfig({ ...validConfig, port: 0 });
|
|
242
|
+
expect(result.valid).toBe(false);
|
|
243
|
+
expect(result.errors).toContain('validation.botConfig.port.range');
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it('returns error when port exceeds 65535', () => {
|
|
247
|
+
const result = validateBotConfig({ ...validConfig, port: 70000 });
|
|
248
|
+
expect(result.valid).toBe(false);
|
|
249
|
+
expect(result.errors).toContain('validation.botConfig.port.range');
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it('returns error when username is missing', () => {
|
|
253
|
+
const result = validateBotConfig({ ...validConfig, username: '' });
|
|
254
|
+
expect(result.valid).toBe(false);
|
|
255
|
+
expect(result.errors).toContain('validation.botConfig.username.required');
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it('accepts optional password', () => {
|
|
259
|
+
const result = validateBotConfig({ ...validConfig, password: 'secret' });
|
|
260
|
+
expect(result.valid).toBe(true);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it('returns error when password is not a string', () => {
|
|
264
|
+
const result = validateBotConfig({ ...validConfig, password: 123 });
|
|
265
|
+
expect(result.valid).toBe(false);
|
|
266
|
+
expect(result.errors).toContain('validation.botConfig.password.type');
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it('accepts optional version', () => {
|
|
270
|
+
const result = validateBotConfig({ ...validConfig, version: '1.19.4' });
|
|
271
|
+
expect(result.valid).toBe(true);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it('accepts optional proxy as object', () => {
|
|
275
|
+
const result = validateBotConfig({ ...validConfig, proxy: { host: 'proxy.example.com', port: 8080 } });
|
|
276
|
+
expect(result.valid).toBe(true);
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it('returns error when proxy is not an object', () => {
|
|
280
|
+
const result = validateBotConfig({ ...validConfig, proxy: 'proxy.example.com' });
|
|
281
|
+
expect(result.valid).toBe(false);
|
|
282
|
+
expect(result.errors).toContain('validation.botConfig.proxy.type');
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
it('returns error for non-object input', () => {
|
|
286
|
+
const result = validateBotConfig(null);
|
|
287
|
+
expect(result.valid).toBe(false);
|
|
288
|
+
expect(result.errors).toContain('validation.botConfig.type');
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it('accepts port boundary values 1 and 65535', () => {
|
|
292
|
+
expect(validateBotConfig({ ...validConfig, port: 1 }).valid).toBe(true);
|
|
293
|
+
expect(validateBotConfig({ ...validConfig, port: 65535 }).valid).toBe(true);
|
|
294
|
+
});
|
|
295
|
+
});
|
|
296
|
+
});
|
|
@@ -16,7 +16,7 @@ function initializeBotApiNamespace(io) {
|
|
|
16
16
|
console.log(`[Bot API] Клиент подключился к боту ID: ${socket.botId} (ключ: ${socket.keyPrefix})`);
|
|
17
17
|
|
|
18
18
|
socket.join(`bot_${socket.botId}`);
|
|
19
|
-
|
|
19
|
+
socket.join(`key_${socket.keyId}`);
|
|
20
20
|
|
|
21
21
|
const isOnline = botManager.isBotRunning(socket.botId);
|
|
22
22
|
socket.emit('bot:status', { online: isOnline });
|