spiralcord-full 2.1.0 → 2.2.7

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/src/test.js CHANGED
@@ -1,5 +1,4 @@
1
1
  const fs = require('fs').promises;
2
- const fsSync = require('fs');
3
2
  const path = require('path');
4
3
 
5
4
  class SpiralTester {
@@ -147,31 +146,59 @@ class SpiralTester {
147
146
  const commandsMatch = code.match(/commands\s*:\s*\{/);
148
147
  const hooksMatch = code.match(/hooks\s*:\s*\{/);
149
148
  const apiMatch = code.match(/api\s*:\s*\{/);
149
+ const keywordsMatch = code.match(/keywords\s*:\s*\{/);
150
+ const initMatch = code.match(/init\s*:/);
150
151
 
151
- if (!commandsMatch && !hooksMatch && !apiMatch) {
152
- result.issues.push('No commands, hooks, or api exported');
153
- this.warnings.push(`${name}: No commands, hooks, or api exported`);
152
+ if (!commandsMatch && !hooksMatch && !apiMatch && !keywordsMatch && !initMatch) {
153
+ result.issues.push('No commands, hooks, api, keywords, or init exported');
154
+ this.warnings.push(`${name}: No commands, hooks, api, keywords, or init exported`);
154
155
  }
155
156
 
156
157
  if (commandsMatch) {
157
- const cmdRegex = /(\w+)\s*:\s*\{/g;
158
- let match;
159
- while ((match = cmdRegex.exec(code)) !== null) {
160
- if (match[1] !== 'commands' && match[1] !== 'hooks' && match[1] !== 'api' && match[1] !== 'init') {
161
- result.commands++;
162
- this.allCommands.set(match[1], name);
158
+ const cmdBlockStart = code.indexOf(commandsMatch[0]);
159
+ if (cmdBlockStart !== -1) {
160
+ let depth = 0;
161
+ let blockContent = '';
162
+ let inBlock = false;
163
+ for (let i = cmdBlockStart; i < code.length; i++) {
164
+ if (code[i] === '{') { depth++; inBlock = true; }
165
+ if (inBlock) blockContent += code[i];
166
+ if (code[i] === '}') { depth--; if (depth === 0) break; }
167
+ }
168
+
169
+ const cmdRegex = /(\w+)\s*:\s*\{/g;
170
+ let match;
171
+ while ((match = cmdRegex.exec(blockContent)) !== null) {
172
+ if (match[1] !== 'commands' && match[1] !== 'hooks' && match[1] !== 'api' && match[1] !== 'init') {
173
+ result.commands++;
174
+ if (!this.allCommands.has(match[1])) this.allCommands.set(match[1], []);
175
+ this.allCommands.get(match[1]).push(name);
176
+ }
163
177
  }
164
178
  }
165
179
  }
166
180
 
167
181
  if (hooksMatch) {
168
- const hookRegex = /(\w+)\s*:\s*async\s*\(/g;
169
- let match;
170
- while ((match = hookRegex.exec(code)) !== null) {
171
- if (['message_received', 'bot_ready', 'interaction_received', 'presence_update', 'guild_joined', 'guild_left', 'voice_state_update', 'reaction_add', 'reaction_remove', 'bot_shutdown'].includes(match[1])) {
172
- result.hooks++;
173
- if (!this.allHooks.has(match[1])) this.allHooks.set(match[1], []);
174
- this.allHooks.get(match[1]).push(name);
182
+ const hookBlockStart = code.indexOf(hooksMatch[0]);
183
+ if (hookBlockStart !== -1) {
184
+ let depth = 0;
185
+ let blockContent = '';
186
+ let inBlock = false;
187
+ for (let i = hookBlockStart; i < code.length; i++) {
188
+ if (code[i] === '{') { depth++; inBlock = true; }
189
+ if (inBlock) blockContent += code[i];
190
+ if (code[i] === '}') { depth--; if (depth === 0) break; }
191
+ }
192
+
193
+ const knownHooks = ['message_received', 'bot_ready', 'interaction_received', 'presence_update', 'guild_joined', 'guild_left', 'voice_state_update', 'reaction_add', 'reaction_remove', 'bot_shutdown'];
194
+ const hookRegex = /(\w+)\s*:\s*async\s*(?:\(|function\s*\()/g;
195
+ let match;
196
+ while ((match = hookRegex.exec(blockContent)) !== null) {
197
+ if (knownHooks.includes(match[1])) {
198
+ result.hooks++;
199
+ if (!this.allHooks.has(match[1])) this.allHooks.set(match[1], []);
200
+ this.allHooks.get(match[1]).push(name);
201
+ }
175
202
  }
176
203
  }
177
204
  }
@@ -179,13 +206,13 @@ class SpiralTester {
179
206
  const requireMatches = code.match(/require\(['"]([^'"]+)['"]\)/g);
180
207
  if (requireMatches) {
181
208
  for (const req of requireMatches) {
182
- const module = req.match(/require\(['"]([^'"]+)['"]\)/)[1];
183
- if (module.startsWith('.') || module.startsWith('/')) continue;
209
+ const mod = req.match(/require\(['"]([^'"]+)['"]\)/)[1];
210
+ if (mod.startsWith('.') || mod.startsWith('/')) continue;
184
211
  try {
185
- require.resolve(module);
212
+ require.resolve(mod, { paths: [path.dirname(indexPath)] });
186
213
  } catch {
187
- result.issues.push(`Missing dependency: ${module}`);
188
- this.warnings.push(`${name}: Missing dependency - ${module}`);
214
+ result.issues.push(`Missing dependency: ${mod}`);
215
+ this.warnings.push(`${name}: Missing dependency - ${mod}`);
189
216
  }
190
217
  }
191
218
  }
@@ -215,17 +242,22 @@ class SpiralTester {
215
242
 
216
243
  if (line === '' || line.startsWith('#')) continue;
217
244
 
218
- if (line.startsWith('COMMAND ')) {
219
- const match = line.match(/COMMAND\s+(\S+)\s+"([^"]+)"/);
245
+ if (line.startsWith('COMMAND ') || line.startsWith('SLASH_COMMAND ')) {
246
+ const pattern = line.startsWith('SLASH_COMMAND ')
247
+ ? /SLASH_COMMAND\s+(\S+)\s+"([^"]+)"/
248
+ : /COMMAND\s+(\S+)\s+"([^"]+)"/;
249
+ const match = line.match(pattern);
220
250
  if (!match) {
221
251
  result.status = 'error';
222
- result.issues.push(`Line ${i + 1}: Invalid COMMAND syntax`);
223
- this.errors.push(`${name}: Line ${i + 1} - Invalid COMMAND syntax`);
252
+ const keyword = line.startsWith('SLASH_COMMAND') ? 'SLASH_COMMAND' : 'COMMAND';
253
+ result.issues.push(`Line ${i + 1}: Invalid ${keyword} syntax. Use: ${keyword} name "description"`);
254
+ this.errors.push(`${name}: Line ${i + 1} - Invalid ${keyword} syntax`);
224
255
  } else {
225
256
  inCommand = true;
226
257
  commandName = match[1];
227
258
  result.commands++;
228
- this.allCommands.set(commandName, name);
259
+ if (!this.allCommands.has(commandName)) this.allCommands.set(commandName, []);
260
+ this.allCommands.get(commandName).push(name);
229
261
  }
230
262
  } else if (line === 'END') {
231
263
  if (inResponsePool) {
@@ -238,11 +270,10 @@ class SpiralTester {
238
270
  if (line.startsWith('RESPONSE_POOL')) {
239
271
  inResponsePool = true;
240
272
  } else if (!inResponsePool) {
241
- if (!line.startsWith('ALIASES') && !line.startsWith('COOLDOWN') &&
242
- !line.startsWith('REQUIRES') && !line.startsWith('RESPONSE ') &&
243
- !line.startsWith('EMBED') && !line.startsWith('TITLE') &&
244
- !line.startsWith('DESCRIPTION')) {
245
- result.issues.push(`Line ${i + 1}: Unknown DSL keyword: ${line.split(' ')[0]}`);
273
+ const validKeywords = ['ALIASES', 'COOLDOWN', 'REQUIRES_ARGS', 'REQUIRES_PERMISSION', 'RESPONSE', 'EMBED', 'TITLE', 'DESCRIPTION', 'BUTTON', 'SELECT', 'DB_SET', 'DB_GET', 'DB_ADD', 'DB_DELETE', 'DB_RESPONSE'];
274
+ const firstWord = line.split(' ')[0];
275
+ if (!validKeywords.includes(firstWord)) {
276
+ result.issues.push(`Line ${i + 1}: Unknown DSL keyword: ${firstWord}`);
246
277
  this.warnings.push(`${name}: Line ${i + 1} - Unknown DSL keyword`);
247
278
  }
248
279
  }
@@ -273,10 +304,11 @@ class SpiralTester {
273
304
  const hasSchema = await this.fileExists(configSchemaPath);
274
305
  const hasConfig = await this.fileExists(configPath);
275
306
 
307
+ let schema = null;
276
308
  if (hasSchema) {
277
309
  try {
278
310
  const schemaData = await fs.readFile(configSchemaPath, 'utf-8');
279
- JSON.parse(schemaData);
311
+ schema = JSON.parse(schemaData);
280
312
  } catch (e) {
281
313
  result.issues.push(`Invalid config.schema.json: ${e.message}`);
282
314
  this.warnings.push(`${name}: Invalid config.schema.json`);
@@ -288,28 +320,21 @@ class SpiralTester {
288
320
  const configData = await fs.readFile(configPath, 'utf-8');
289
321
  const config = JSON.parse(configData);
290
322
 
291
- if (hasSchema) {
292
- try {
293
- const schemaData = await fs.readFile(configSchemaPath, 'utf-8');
294
- const schema = JSON.parse(schemaData);
295
-
296
- if (schema.properties) {
297
- for (const [key, prop] of Object.entries(schema.properties)) {
298
- if (prop.type === 'string' && config[key] !== undefined && typeof config[key] !== 'string') {
299
- result.issues.push(`Config "${key}" should be string, got ${typeof config[key]}`);
300
- this.warnings.push(`${name}: Config "${key}" type mismatch`);
301
- }
302
- if (prop.type === 'number' && config[key] !== undefined && typeof config[key] !== 'number') {
303
- result.issues.push(`Config "${key}" should be number, got ${typeof config[key]}`);
304
- this.warnings.push(`${name}: Config "${key}" type mismatch`);
305
- }
306
- if (prop.type === 'boolean' && config[key] !== undefined && typeof config[key] !== 'boolean') {
307
- result.issues.push(`Config "${key}" should be boolean, got ${typeof config[key]}`);
308
- this.warnings.push(`${name}: Config "${key}" type mismatch`);
309
- }
310
- }
323
+ if (schema && schema.properties) {
324
+ for (const [key, prop] of Object.entries(schema.properties)) {
325
+ if (prop.type === 'string' && config[key] !== undefined && typeof config[key] !== 'string') {
326
+ result.issues.push(`Config "${key}" should be string, got ${typeof config[key]}`);
327
+ this.warnings.push(`${name}: Config "${key}" type mismatch`);
328
+ }
329
+ if (prop.type === 'number' && config[key] !== undefined && typeof config[key] !== 'number') {
330
+ result.issues.push(`Config "${key}" should be number, got ${typeof config[key]}`);
331
+ this.warnings.push(`${name}: Config "${key}" type mismatch`);
311
332
  }
312
- } catch {}
333
+ if (prop.type === 'boolean' && config[key] !== undefined && typeof config[key] !== 'boolean') {
334
+ result.issues.push(`Config "${key}" should be boolean, got ${typeof config[key]}`);
335
+ this.warnings.push(`${name}: Config "${key}" type mismatch`);
336
+ }
337
+ }
313
338
  }
314
339
  } catch (e) {
315
340
  result.issues.push(`Invalid config.json: ${e.message}`);
@@ -321,12 +346,9 @@ class SpiralTester {
321
346
  }
322
347
 
323
348
  checkCommandConflicts() {
324
- const seen = new Map();
325
- for (const [cmd, plugin] of this.allCommands) {
326
- if (seen.has(cmd)) {
327
- this.warnings.push(`Command conflict: "${cmd}" in ${seen.get(cmd)} and ${plugin}`);
328
- } else {
329
- seen.set(cmd, plugin);
349
+ for (const [cmd, plugins] of this.allCommands) {
350
+ if (plugins.length > 1) {
351
+ this.warnings.push(`Command conflict: "${cmd}" in ${plugins.join(', ')}`);
330
352
  }
331
353
  }
332
354
  }
@@ -348,9 +370,9 @@ class SpiralTester {
348
370
  }
349
371
  }
350
372
 
351
- async fileExists(path) {
373
+ async fileExists(filePath) {
352
374
  try {
353
- await fs.access(path);
375
+ await fs.access(filePath);
354
376
  return true;
355
377
  } catch {
356
378
  return false;
@@ -358,4 +380,4 @@ class SpiralTester {
358
380
  }
359
381
  }
360
382
 
361
- module.exports = SpiralTester;
383
+ module.exports = SpiralTester;
@@ -0,0 +1,100 @@
1
+ export interface SpiralConfig {
2
+ name: string;
3
+ token: string;
4
+ prefix: string;
5
+ intents: string[];
6
+ clientId: string;
7
+ disabledCommands: string[];
8
+ }
9
+
10
+ export interface SpiralPluginManifest {
11
+ name: string;
12
+ version: string;
13
+ description: string;
14
+ author?: string;
15
+ dependencies?: Record<string, string>;
16
+ collision_policy?: 'error' | 'first-wins' | 'last-wins';
17
+ builtin?: boolean;
18
+ }
19
+
20
+ export interface SpiralPluginConfig {
21
+ [key: string]: any;
22
+ }
23
+
24
+ export interface SpiralCommand {
25
+ description: string;
26
+ usage?: string;
27
+ aliases?: string[];
28
+ slash?: boolean;
29
+ priority?: number;
30
+ execute: (message: any, args: string[], runtime: SpiralRuntime) => Promise<void>;
31
+ }
32
+
33
+ export interface SpiralHooks {
34
+ bot_ready?: (payload: { client: any; user: any; config: SpiralConfig }, runtime: SpiralRuntime) => Promise<void>;
35
+ message_received?: (payload: { message: any; user: any; guild: any; channel: any }, runtime: SpiralRuntime) => Promise<void>;
36
+ interaction_received?: (payload: { interaction: any; user: any; guild: any; channel: any }, runtime: SpiralRuntime) => Promise<void>;
37
+ presence_update?: (payload: { oldPresence: any; newPresence: any; user: any; guild: any }, runtime: SpiralRuntime) => Promise<void>;
38
+ voice_state_update?: (payload: { oldState: any; newState: any; user: any; guild: any }, runtime: SpiralRuntime) => Promise<void>;
39
+ reaction_add?: (payload: { reaction: any; user: any; message: any }, runtime: SpiralRuntime) => Promise<void>;
40
+ reaction_remove?: (payload: { reaction: any; user: any; message: any }, runtime: SpiralRuntime) => Promise<void>;
41
+ guild_joined?: (payload: { guild: any }, runtime: SpiralRuntime) => Promise<void>;
42
+ guild_left?: (payload: { guild: any }, runtime: SpiralRuntime) => Promise<void>;
43
+ bot_shutdown?: (payload: { client: any }, runtime: SpiralRuntime) => Promise<void>;
44
+ [key: string]: ((payload: any, runtime: SpiralRuntime) => Promise<void>) | undefined;
45
+ }
46
+
47
+ export interface SpiralPlugin {
48
+ init?: (config: SpiralPluginConfig, runtime: SpiralRuntime) => Promise<void>;
49
+ commands?: Record<string, SpiralCommand>;
50
+ keywords?: Record<string, (message: any, args: string[], runtime: SpiralRuntime) => Promise<void>>;
51
+ hooks?: SpiralHooks;
52
+ api?: Record<string, (...args: any[]) => any>;
53
+ }
54
+
55
+ export interface SpiralDB {
56
+ get(key: string, defaultValue?: any): any;
57
+ set(key: string, value: any): any;
58
+ add(key: string, amount: number): number;
59
+ delete(key: string): boolean;
60
+ has(key: string): boolean;
61
+ all(): Record<string, any>;
62
+ clear(): void;
63
+ }
64
+
65
+ export interface SpiralRuntime {
66
+ config: SpiralConfig;
67
+ client: any;
68
+ db: SpiralDB;
69
+ startTime: number;
70
+ start(): Promise<void>;
71
+ restart(): Promise<void>;
72
+ stop(): Promise<void>;
73
+ getUptime(): number;
74
+ getCommand(name: string): any;
75
+ getAllCommands(): Map<string, any>;
76
+ getPluginConfig(pluginName: string): SpiralPluginConfig;
77
+ getPluginAPI(pluginName: string): Record<string, (...args: any[]) => any> | null;
78
+ emitHook(eventName: string, payload: any): Promise<void>;
79
+ emitHookSerial(eventName: string, payload: any): Promise<void>;
80
+ emitHookWithResult(eventName: string, payload: any): Promise<boolean>;
81
+ }
82
+
83
+ export interface SpiralTester {
84
+ test(): Promise<{
85
+ passed: number;
86
+ warnings: number;
87
+ errors: number;
88
+ results: Array<{
89
+ name: string;
90
+ status: 'ok' | 'warning' | 'error';
91
+ commands: number;
92
+ hooks: number;
93
+ issues: string[];
94
+ }>;
95
+ details: {
96
+ warnings: string[];
97
+ errors: string[];
98
+ };
99
+ }>;
100
+ }
package/src/web/server.js CHANGED
@@ -10,12 +10,33 @@ class SpiralWeb {
10
10
  this.server = http.createServer(this.app);
11
11
  this.port = process.env.PORT || 3000;
12
12
 
13
- this.app.use(express.json());
13
+ this.app.use(express.json({ limit: '1mb' }));
14
+
15
+ this.app.use((req, res, next) => {
16
+ res.setHeader('X-Content-Type-Options', 'nosniff');
17
+ res.setHeader('X-Frame-Options', 'DENY');
18
+ res.setHeader('X-XSS-Protection', '1; mode=block');
19
+ next();
20
+ });
21
+
14
22
  this.app.use(express.static(path.join(__dirname, 'public')));
15
23
 
16
24
  this.setupRoutes();
17
25
  }
18
26
 
27
+ _sanitizeName(name) {
28
+ if (!name || typeof name !== 'string') return null;
29
+ const sanitized = name.replace(/[^a-zA-Z0-9_-]/g, '');
30
+ if (sanitized !== name || sanitized.length === 0 || sanitized.length > 100) return null;
31
+ if (sanitized.includes('..') || sanitized.startsWith('.')) return null;
32
+ return sanitized;
33
+ }
34
+
35
+ _sanitizeError(error) {
36
+ const msg = error.message || String(error);
37
+ return msg.replace(/\/[^\s:]+/g, '[path]');
38
+ }
39
+
19
40
  setupRoutes() {
20
41
  this.app.get('/api/config', (req, res) => {
21
42
  try {
@@ -27,65 +48,57 @@ class SpiralWeb {
27
48
  res.json({ success: true, config: {} });
28
49
  }
29
50
  } catch (error) {
30
- res.status(500).json({ success: false, error: error.message });
51
+ res.status(500).json({ success: false, error: this._sanitizeError(error) });
31
52
  }
32
53
  });
33
54
 
34
55
  this.app.post('/api/config', (req, res) => {
35
56
  try {
36
57
  const { config } = req.body;
58
+ if (!config || typeof config !== 'object' || Array.isArray(config)) {
59
+ return res.status(400).json({ success: false, error: 'Config must be a JSON object' });
60
+ }
37
61
  const configPath = path.join(process.cwd(), 'spiral.json');
38
62
  fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
39
63
  res.json({ success: true });
40
64
  } catch (error) {
41
- res.status(500).json({ success: false, error: error.message });
65
+ res.status(500).json({ success: false, error: this._sanitizeError(error) });
42
66
  }
43
67
  });
44
68
 
45
69
  this.app.get('/api/plugins', (req, res) => {
46
70
  try {
47
71
  const pluginsDir = path.join(process.cwd(), 'plugins');
48
- const builtinDir = path.join(__dirname, '..', 'plugins', 'builtin');
49
-
50
72
  const plugins = [];
51
73
 
52
- if (fs.existsSync(builtinDir)) {
53
- const folders = fs.readdirSync(builtinDir);
54
- for (const folder of folders) {
55
- const manifestPath = path.join(builtinDir, folder, 'plugin.json');
56
- if (fs.existsSync(manifestPath)) {
57
- const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
58
- plugins.push({ ...manifest, builtin: true, source: 'builtin' });
59
- }
60
- }
61
- }
62
-
63
74
  if (fs.existsSync(pluginsDir)) {
64
- const folders = fs.readdirSync(pluginsDir);
75
+ const entries = fs.readdirSync(pluginsDir, { withFileTypes: true });
76
+ const folders = entries.filter(e => e.isDirectory() && !e.name.startsWith('.'));
65
77
  for (const folder of folders) {
66
- const manifestPath = path.join(pluginsDir, folder, 'plugin.json');
67
- if (fs.existsSync(manifestPath)) {
68
- const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
69
- plugins.push({ ...manifest, builtin: false, source: 'user' });
70
- }
78
+ try {
79
+ const manifestPath = path.join(pluginsDir, folder.name, 'plugin.json');
80
+ if (fs.existsSync(manifestPath)) {
81
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
82
+ plugins.push({ ...manifest, builtin: false, source: 'user' });
83
+ }
84
+ } catch {}
71
85
  }
72
86
  }
73
87
 
74
88
  res.json({ success: true, plugins });
75
89
  } catch (error) {
76
- res.status(500).json({ success: false, error: error.message });
90
+ res.status(500).json({ success: false, error: this._sanitizeError(error) });
77
91
  }
78
92
  });
79
93
 
80
94
  this.app.get('/api/plugins/:name', (req, res) => {
81
95
  try {
82
- const name = req.params.name;
83
- let pluginPath = path.join(__dirname, '..', 'plugins', 'builtin', name);
84
-
85
- if (!fs.existsSync(pluginPath)) {
86
- pluginPath = path.join(process.cwd(), 'plugins', name);
96
+ const name = this._sanitizeName(req.params.name);
97
+ if (!name) {
98
+ return res.status(400).json({ success: false, error: 'Invalid plugin name' });
87
99
  }
88
100
 
101
+ const pluginPath = path.join(process.cwd(), 'plugins', name);
89
102
  if (!fs.existsSync(pluginPath)) {
90
103
  return res.status(404).json({ success: false, error: 'Plugin not found' });
91
104
  }
@@ -106,20 +119,23 @@ class SpiralWeb {
106
119
 
107
120
  res.json({ success: true, manifest, configSchema, config });
108
121
  } catch (error) {
109
- res.status(500).json({ success: false, error: error.message });
122
+ res.status(500).json({ success: false, error: this._sanitizeError(error) });
110
123
  }
111
124
  });
112
125
 
113
126
  this.app.put('/api/plugins/:name/config', (req, res) => {
114
127
  try {
115
- const name = req.params.name;
116
- const { config } = req.body;
128
+ const name = this._sanitizeName(req.params.name);
129
+ if (!name) {
130
+ return res.status(400).json({ success: false, error: 'Invalid plugin name' });
131
+ }
117
132
 
118
- let pluginPath = path.join(__dirname, '..', 'plugins', 'builtin', name);
119
- if (!fs.existsSync(pluginPath)) {
120
- pluginPath = path.join(process.cwd(), 'plugins', name);
133
+ const { config } = req.body;
134
+ if (!config || typeof config !== 'object' || Array.isArray(config)) {
135
+ return res.status(400).json({ success: false, error: 'Config must be a JSON object' });
121
136
  }
122
137
 
138
+ const pluginPath = path.join(process.cwd(), 'plugins', name);
123
139
  if (!fs.existsSync(pluginPath)) {
124
140
  return res.status(404).json({ success: false, error: 'Plugin not found' });
125
141
  }
@@ -128,7 +144,7 @@ class SpiralWeb {
128
144
  fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
129
145
  res.json({ success: true });
130
146
  } catch (error) {
131
- res.status(500).json({ success: false, error: error.message });
147
+ res.status(500).json({ success: false, error: this._sanitizeError(error) });
132
148
  }
133
149
  });
134
150
 
@@ -142,7 +158,11 @@ class SpiralWeb {
142
158
 
143
159
  this.app.get('/api/plugins/:name/api', (req, res) => {
144
160
  try {
145
- const name = req.params.name;
161
+ const name = this._sanitizeName(req.params.name);
162
+ if (!name) {
163
+ return res.status(400).json({ success: false, error: 'Invalid plugin name' });
164
+ }
165
+
146
166
  const api = this.runtime?.pluginManager?.getPluginAPI(name);
147
167
  if (api) {
148
168
  res.json({ success: true, api: Object.keys(api) });
@@ -150,7 +170,7 @@ class SpiralWeb {
150
170
  res.json({ success: true, api: [] });
151
171
  }
152
172
  } catch (error) {
153
- res.status(500).json({ success: false, error: error.message });
173
+ res.status(500).json({ success: false, error: this._sanitizeError(error) });
154
174
  }
155
175
  });
156
176
 
@@ -165,20 +185,28 @@ class SpiralWeb {
165
185
  }
166
186
  res.json({ success: true, hooks });
167
187
  } catch (error) {
168
- res.status(500).json({ success: false, error: error.message });
188
+ res.status(500).json({ success: false, error: this._sanitizeError(error) });
169
189
  }
170
190
  });
171
191
  }
172
192
 
173
193
  start() {
194
+ this.server.on('error', (error) => {
195
+ if (error.code === 'EADDRINUSE') {
196
+ console.error(`Port ${this.port} is already in use`);
197
+ } else {
198
+ console.error('Server error:', error.message);
199
+ }
200
+ });
201
+
174
202
  this.server.listen(this.port, () => {
175
203
  console.log(`Spiralcord Web Manager running at http://localhost:${this.port}`);
176
204
  });
177
205
  }
178
206
 
179
- stop() {
180
- this.server.close();
207
+ stop(callback) {
208
+ this.server.close(callback);
181
209
  }
182
210
  }
183
211
 
184
- module.exports = SpiralWeb;
212
+ module.exports = SpiralWeb;