lumisjs 1.0.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/LICENSE +21 -0
- package/README.md +274 -0
- package/SECURITY.md +160 -0
- package/index.js +90 -0
- package/package.json +68 -0
- package/src/cache/CacheManager.js +393 -0
- package/src/cache/MemoryAdapter.js +259 -0
- package/src/cache/MultiLevelCache.js +362 -0
- package/src/cache/RedisAdapter.js +329 -0
- package/src/cache/SQLiteAdapter.js +280 -0
- package/src/cache/index.js +15 -0
- package/src/client/Client.js +554 -0
- package/src/client/ShardingManager.js +274 -0
- package/src/config/ConfigValidator.js +172 -0
- package/src/config/index.js +7 -0
- package/src/dashboard/DashboardManager.js +362 -0
- package/src/datagen/DataGenerator.js +47 -0
- package/src/datagen/apis/GraphqlMocker.js +16 -0
- package/src/datagen/apis/OpenApiMocker.js +18 -0
- package/src/datagen/cli.js +130 -0
- package/src/datagen/formatters/csv.js +45 -0
- package/src/datagen/formatters/index.js +15 -0
- package/src/datagen/formatters/json.js +33 -0
- package/src/datagen/formatters/mongo.js +22 -0
- package/src/datagen/formatters/sql.js +32 -0
- package/src/datagen/index.js +17 -0
- package/src/datagen/plugin/PluginManager.js +43 -0
- package/src/datagen/plugins/example-plugin.js +21 -0
- package/src/datagen/schema/SchemaGenerator.js +172 -0
- package/src/datagen/schema/loadSchema.js +14 -0
- package/src/datagen/utils/seed.js +26 -0
- package/src/di/ServiceContainer.js +229 -0
- package/src/errors/ErrorCodes.js +110 -0
- package/src/errors/LumisError.js +85 -0
- package/src/game/EconomyManager.js +161 -0
- package/src/game/GameSessionManager.js +76 -0
- package/src/game/GuildManager.js +197 -0
- package/src/game/InventoryManager.js +140 -0
- package/src/game/LevelingSystem.js +194 -0
- package/src/game/MusicManager.js +587 -0
- package/src/health/HealthChecker.js +170 -0
- package/src/health/index.js +7 -0
- package/src/performance/PerformanceMonitor.js +244 -0
- package/src/performance/index.js +7 -0
- package/src/security/InputSanitizer.js +185 -0
- package/src/security/SecurityMiddleware.js +231 -0
- package/src/security/index.js +9 -0
- package/src/shutdown/GracefulShutdown.js +159 -0
- package/src/shutdown/index.js +7 -0
- package/src/utils/StructuredLogger.js +118 -0
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { EventEmitter } = require('node:events');
|
|
4
|
+
const Logger = require('../utils/Logger');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Health Checker
|
|
8
|
+
* Monitors system health and provides health check endpoints.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
class HealthChecker extends EventEmitter {
|
|
12
|
+
constructor(options = {}) {
|
|
13
|
+
super();
|
|
14
|
+
|
|
15
|
+
this.options = {
|
|
16
|
+
checkInterval: options.checkInterval || 30000, // 30 seconds
|
|
17
|
+
timeout: options.timeout || 5000, // 5 seconds
|
|
18
|
+
...options,
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
this.logger = new Logger({ prefix: 'HealthCheck', level: 'info' });
|
|
22
|
+
|
|
23
|
+
this.checks = new Map();
|
|
24
|
+
this.status = 'healthy';
|
|
25
|
+
this.lastCheck = null;
|
|
26
|
+
this._interval = null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Register a health check.
|
|
31
|
+
* @param {string} name
|
|
32
|
+
* @param {Function} checkFn - Function that returns a Promise resolving to { status: 'ok'|'degraded'|'down', message?: string }
|
|
33
|
+
*/
|
|
34
|
+
register(name, checkFn) {
|
|
35
|
+
if (typeof checkFn !== 'function') {
|
|
36
|
+
throw new TypeError('Health check must be a function');
|
|
37
|
+
}
|
|
38
|
+
this.checks.set(name, checkFn);
|
|
39
|
+
this.logger.info(`Registered health check: ${name}`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Unregister a health check.
|
|
44
|
+
* @param {string} name
|
|
45
|
+
*/
|
|
46
|
+
unregister(name) {
|
|
47
|
+
this.checks.delete(name);
|
|
48
|
+
this.logger.info(`Unregistered health check: ${name}`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Run all health checks.
|
|
53
|
+
* @returns {Promise<object>}
|
|
54
|
+
*/
|
|
55
|
+
async check() {
|
|
56
|
+
const results = {};
|
|
57
|
+
let overallStatus = 'healthy';
|
|
58
|
+
const now = Date.now();
|
|
59
|
+
|
|
60
|
+
for (const [name, checkFn] of this.checks) {
|
|
61
|
+
try {
|
|
62
|
+
const result = await Promise.race([
|
|
63
|
+
checkFn(),
|
|
64
|
+
new Promise((_, reject) =>
|
|
65
|
+
setTimeout(() => reject(new Error('Health check timeout')), this.options.timeout)
|
|
66
|
+
),
|
|
67
|
+
]);
|
|
68
|
+
|
|
69
|
+
results[name] = {
|
|
70
|
+
status: result.status || 'ok',
|
|
71
|
+
message: result.message || '',
|
|
72
|
+
timestamp: now,
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
if (result.status === 'down') {
|
|
76
|
+
overallStatus = 'unhealthy';
|
|
77
|
+
} else if (result.status === 'degraded' && overallStatus === 'healthy') {
|
|
78
|
+
overallStatus = 'degraded';
|
|
79
|
+
}
|
|
80
|
+
} catch (error) {
|
|
81
|
+
results[name] = {
|
|
82
|
+
status: 'down',
|
|
83
|
+
message: error.message,
|
|
84
|
+
timestamp: now,
|
|
85
|
+
};
|
|
86
|
+
overallStatus = 'unhealthy';
|
|
87
|
+
this.logger.error(`Health check failed: ${name}`, error);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
this.status = overallStatus;
|
|
92
|
+
this.lastCheck = now;
|
|
93
|
+
|
|
94
|
+
const healthReport = {
|
|
95
|
+
status: overallStatus,
|
|
96
|
+
timestamp: now,
|
|
97
|
+
uptime: process.uptime(),
|
|
98
|
+
checks: results,
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
this.emit('check', healthReport);
|
|
102
|
+
|
|
103
|
+
if (overallStatus !== 'healthy') {
|
|
104
|
+
this.emit('unhealthy', healthReport);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return healthReport;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Start periodic health checks.
|
|
112
|
+
*/
|
|
113
|
+
start() {
|
|
114
|
+
if (this._interval) return;
|
|
115
|
+
|
|
116
|
+
this._interval = setInterval(() => this.check(), this.options.checkInterval);
|
|
117
|
+
if (this._interval.unref) this._interval.unref();
|
|
118
|
+
|
|
119
|
+
this.logger.info('Health checker started');
|
|
120
|
+
this.emit('start');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Stop periodic health checks.
|
|
125
|
+
*/
|
|
126
|
+
stop() {
|
|
127
|
+
if (this._interval) {
|
|
128
|
+
clearInterval(this._interval);
|
|
129
|
+
this._interval = null;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
this.logger.info('Health checker stopped');
|
|
133
|
+
this.emit('stop');
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Get current health status without running checks.
|
|
138
|
+
* @returns {object}
|
|
139
|
+
*/
|
|
140
|
+
getStatus() {
|
|
141
|
+
return {
|
|
142
|
+
status: this.status,
|
|
143
|
+
lastCheck: this.lastCheck,
|
|
144
|
+
uptime: process.uptime(),
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Get a simplified health report for quick checks.
|
|
150
|
+
* @returns {Promise<object>}
|
|
151
|
+
*/
|
|
152
|
+
async quickCheck() {
|
|
153
|
+
const report = await this.check();
|
|
154
|
+
return {
|
|
155
|
+
status: report.status,
|
|
156
|
+
timestamp: report.timestamp,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Destroy the health checker.
|
|
162
|
+
*/
|
|
163
|
+
destroy() {
|
|
164
|
+
this.stop();
|
|
165
|
+
this.checks.clear();
|
|
166
|
+
this.removeAllListeners();
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
module.exports = HealthChecker;
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { EventEmitter } = require('node:events');
|
|
4
|
+
const Logger = require('../utils/Logger');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Performance Monitor
|
|
8
|
+
* Tracks and reports performance metrics for the application.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
class PerformanceMonitor extends EventEmitter {
|
|
12
|
+
constructor(options = {}) {
|
|
13
|
+
super();
|
|
14
|
+
|
|
15
|
+
this.options = {
|
|
16
|
+
sampleInterval: options.sampleInterval || 1000,
|
|
17
|
+
historySize: options.historySize || 60,
|
|
18
|
+
alertThresholds: options.alertThresholds || {
|
|
19
|
+
cpu: 80, // percentage
|
|
20
|
+
memory: 80, // percentage
|
|
21
|
+
eventLoopDelay: 100, // milliseconds
|
|
22
|
+
},
|
|
23
|
+
...options,
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
this.logger = new Logger({ prefix: 'PerfMon', level: 'info' });
|
|
27
|
+
|
|
28
|
+
this.metrics = {
|
|
29
|
+
cpu: [],
|
|
30
|
+
memory: [],
|
|
31
|
+
eventLoopDelay: [],
|
|
32
|
+
eventLoopUtilization: [],
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
this._monitoring = false;
|
|
36
|
+
this._interval = null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Start performance monitoring.
|
|
41
|
+
*/
|
|
42
|
+
start() {
|
|
43
|
+
if (this._monitoring) return;
|
|
44
|
+
|
|
45
|
+
this._monitoring = true;
|
|
46
|
+
this._interval = setInterval(() => this._sample(), this.options.sampleInterval);
|
|
47
|
+
if (this._interval.unref) this._interval.unref();
|
|
48
|
+
|
|
49
|
+
this.logger.info('Performance monitoring started');
|
|
50
|
+
this.emit('start');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Stop performance monitoring.
|
|
55
|
+
*/
|
|
56
|
+
stop() {
|
|
57
|
+
if (!this._monitoring) return;
|
|
58
|
+
|
|
59
|
+
this._monitoring = false;
|
|
60
|
+
if (this._interval) {
|
|
61
|
+
clearInterval(this._interval);
|
|
62
|
+
this._interval = null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
this.logger.info('Performance monitoring stopped');
|
|
66
|
+
this.emit('stop');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Get current performance metrics.
|
|
71
|
+
* @returns {Promise<object>}
|
|
72
|
+
*/
|
|
73
|
+
async getMetrics() {
|
|
74
|
+
const cpuUsage = process.cpuUsage();
|
|
75
|
+
const memoryUsage = process.memoryUsage();
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
cpu: {
|
|
79
|
+
user: cpuUsage.user,
|
|
80
|
+
system: cpuUsage.system,
|
|
81
|
+
percent: this._calculateCPUPercent(cpuUsage),
|
|
82
|
+
},
|
|
83
|
+
memory: {
|
|
84
|
+
rss: memoryUsage.rss,
|
|
85
|
+
heapTotal: memoryUsage.heapTotal,
|
|
86
|
+
heapUsed: memoryUsage.heapUsed,
|
|
87
|
+
external: memoryUsage.external,
|
|
88
|
+
arrayBuffers: memoryUsage.arrayBuffers,
|
|
89
|
+
percent: (memoryUsage.heapUsed / memoryUsage.heapTotal) * 100,
|
|
90
|
+
},
|
|
91
|
+
eventLoop: {
|
|
92
|
+
delay: await this._measureEventLoopDelay(),
|
|
93
|
+
utilization: await this._measureEventLoopUtilization(),
|
|
94
|
+
},
|
|
95
|
+
uptime: process.uptime(),
|
|
96
|
+
timestamp: Date.now(),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Get historical metrics.
|
|
102
|
+
* @returns {object}
|
|
103
|
+
*/
|
|
104
|
+
getHistory() {
|
|
105
|
+
return {
|
|
106
|
+
cpu: [...this.metrics.cpu],
|
|
107
|
+
memory: [...this.metrics.memory],
|
|
108
|
+
eventLoopDelay: [...this.metrics.eventLoopDelay],
|
|
109
|
+
eventLoopUtilization: [...this.metrics.eventLoopUtilization],
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Get performance summary.
|
|
115
|
+
* @returns {object}
|
|
116
|
+
*/
|
|
117
|
+
getSummary() {
|
|
118
|
+
const avgCPU = this._average(this.metrics.cpu);
|
|
119
|
+
const avgMemory = this._average(this.metrics.memory);
|
|
120
|
+
const avgEventLoopDelay = this._average(this.metrics.eventLoopDelay);
|
|
121
|
+
const avgEventLoopUtil = this._average(this.metrics.eventLoopUtilization);
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
cpu: {
|
|
125
|
+
average: avgCPU,
|
|
126
|
+
max: Math.max(...this.metrics.cpu, 0),
|
|
127
|
+
min: Math.min(...this.metrics.cpu, 100),
|
|
128
|
+
},
|
|
129
|
+
memory: {
|
|
130
|
+
average: avgMemory,
|
|
131
|
+
max: Math.max(...this.metrics.memory, 0),
|
|
132
|
+
min: Math.min(...this.metrics.memory, 100),
|
|
133
|
+
},
|
|
134
|
+
eventLoop: {
|
|
135
|
+
averageDelay: avgEventLoopDelay,
|
|
136
|
+
maxDelay: Math.max(...this.metrics.eventLoopDelay, 0),
|
|
137
|
+
averageUtilization: avgEventLoopUtil,
|
|
138
|
+
maxUtilization: Math.max(...this.metrics.eventLoopUtilization, 0),
|
|
139
|
+
},
|
|
140
|
+
monitoring: this._monitoring,
|
|
141
|
+
samples: this.metrics.cpu.length,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Reset metrics history.
|
|
147
|
+
*/
|
|
148
|
+
reset() {
|
|
149
|
+
this.metrics = {
|
|
150
|
+
cpu: [],
|
|
151
|
+
memory: [],
|
|
152
|
+
eventLoopDelay: [],
|
|
153
|
+
eventLoopUtilization: [],
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
this.emit('reset');
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** @private */
|
|
160
|
+
async _sample() {
|
|
161
|
+
const metrics = await this.getMetrics();
|
|
162
|
+
|
|
163
|
+
// Add to history
|
|
164
|
+
this._addToHistory('cpu', metrics.cpu.percent);
|
|
165
|
+
this._addToHistory('memory', metrics.memory.percent);
|
|
166
|
+
this._addToHistory('eventLoopDelay', metrics.eventLoop.delay);
|
|
167
|
+
this._addToHistory('eventLoopUtilization', metrics.eventLoop.utilization);
|
|
168
|
+
|
|
169
|
+
// Check thresholds
|
|
170
|
+
this._checkThresholds(metrics);
|
|
171
|
+
|
|
172
|
+
this.emit('sample', metrics);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** @private */
|
|
176
|
+
_addToHistory(type, value) {
|
|
177
|
+
this.metrics[type].push(value);
|
|
178
|
+
|
|
179
|
+
if (this.metrics[type].length > this.options.historySize) {
|
|
180
|
+
this.metrics[type].shift();
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** @private */
|
|
185
|
+
_calculateCPUPercent(cpuUsage) {
|
|
186
|
+
const total = cpuUsage.user + cpuUsage.system;
|
|
187
|
+
// This is a simplified calculation
|
|
188
|
+
return (total / 1000000) * 100;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** @private */
|
|
192
|
+
async _measureEventLoopDelay() {
|
|
193
|
+
return new Promise((resolve) => {
|
|
194
|
+
const start = process.hrtime.bigint();
|
|
195
|
+
setImmediate(() => {
|
|
196
|
+
const delay = Number(process.hrtime.bigint() - start) / 1000000; // Convert to ms
|
|
197
|
+
resolve(delay);
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** @private */
|
|
203
|
+
async _measureEventLoopUtilization() {
|
|
204
|
+
if (process.eventLoopUtilization) {
|
|
205
|
+
const utilization = process.eventLoopUtilization();
|
|
206
|
+
return utilization.utilization;
|
|
207
|
+
}
|
|
208
|
+
return 0;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** @private */
|
|
212
|
+
_average(arr) {
|
|
213
|
+
if (arr.length === 0) return 0;
|
|
214
|
+
return arr.reduce((a, b) => a + b, 0) / arr.length;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** @private */
|
|
218
|
+
_checkThresholds(metrics) {
|
|
219
|
+
const thresholds = this.options.alertThresholds;
|
|
220
|
+
|
|
221
|
+
if (metrics.cpu.percent > thresholds.cpu) {
|
|
222
|
+
this.emit('alert', { type: 'cpu', value: metrics.cpu.percent, threshold: thresholds.cpu });
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (metrics.memory.percent > thresholds.memory) {
|
|
226
|
+
this.emit('alert', { type: 'memory', value: metrics.memory.percent, threshold: thresholds.memory });
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (metrics.eventLoop.delay > thresholds.eventLoopDelay) {
|
|
230
|
+
this.emit('alert', { type: 'eventLoop', value: metrics.eventLoop.delay, threshold: thresholds.eventLoopDelay });
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Destroy the monitor.
|
|
236
|
+
*/
|
|
237
|
+
destroy() {
|
|
238
|
+
this.stop();
|
|
239
|
+
this.reset();
|
|
240
|
+
this.removeAllListeners();
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
module.exports = PerformanceMonitor;
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Input Sanitizer
|
|
5
|
+
* Provides utilities for sanitizing user input to prevent XSS, injection attacks, etc.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
class InputSanitizer {
|
|
9
|
+
/**
|
|
10
|
+
* Sanitize a string by removing potentially dangerous characters.
|
|
11
|
+
* @param {string} input
|
|
12
|
+
* @param {object} [options]
|
|
13
|
+
* @param {boolean} [options.allowHTML=false]
|
|
14
|
+
* @param {boolean} [options.allowScript=false]
|
|
15
|
+
* @param {number} [options.maxLength=1000]
|
|
16
|
+
* @returns {string}
|
|
17
|
+
*/
|
|
18
|
+
static sanitize(input, options = {}) {
|
|
19
|
+
if (typeof input !== 'string') return '';
|
|
20
|
+
|
|
21
|
+
const {
|
|
22
|
+
allowHTML = false,
|
|
23
|
+
allowScript = false,
|
|
24
|
+
maxLength = 1000,
|
|
25
|
+
} = options;
|
|
26
|
+
|
|
27
|
+
let sanitized = input;
|
|
28
|
+
|
|
29
|
+
// Truncate if too long
|
|
30
|
+
if (sanitized.length > maxLength) {
|
|
31
|
+
sanitized = sanitized.substring(0, maxLength);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Remove script tags
|
|
35
|
+
if (!allowScript) {
|
|
36
|
+
sanitized = sanitized.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Remove dangerous HTML tags if not allowed
|
|
40
|
+
if (!allowHTML) {
|
|
41
|
+
const dangerousTags = ['<script', '<iframe', '<object', '<embed', '<form', '<input', '<button'];
|
|
42
|
+
for (const tag of dangerousTags) {
|
|
43
|
+
sanitized = sanitized.replace(new RegExp(tag, 'gi'), '');
|
|
44
|
+
}
|
|
45
|
+
// Remove all HTML tags
|
|
46
|
+
sanitized = sanitized.replace(/<[^>]*>/g, '');
|
|
47
|
+
} else {
|
|
48
|
+
// Still remove dangerous attributes
|
|
49
|
+
sanitized = sanitized.replace(/on\w+\s*=/gi, '');
|
|
50
|
+
sanitized = sanitized.replace(/javascript:/gi, '');
|
|
51
|
+
sanitized = sanitized.replace(/data:/gi, '');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Remove null bytes and other control characters
|
|
55
|
+
sanitized = sanitized.replace(/[\x00-\x1F\x7F]/g, '');
|
|
56
|
+
|
|
57
|
+
return sanitized.trim();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Sanitize a number to prevent injection.
|
|
62
|
+
* @param {any} input
|
|
63
|
+
* @param {object} [options]
|
|
64
|
+
* @param {number} [options.min]
|
|
65
|
+
* @param {number} [options.max]
|
|
66
|
+
* @param {boolean} [options.allowFloat=true]
|
|
67
|
+
* @returns {number|null}
|
|
68
|
+
*/
|
|
69
|
+
static sanitizeNumber(input, options = {}) {
|
|
70
|
+
const { min, max, allowFloat = true } = options;
|
|
71
|
+
|
|
72
|
+
let num = allowFloat ? parseFloat(input) : parseInt(input, 10);
|
|
73
|
+
|
|
74
|
+
if (isNaN(num)) return null;
|
|
75
|
+
|
|
76
|
+
if (min !== undefined && num < min) num = min;
|
|
77
|
+
if (max !== undefined && num > max) num = max;
|
|
78
|
+
|
|
79
|
+
return num;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Sanitize a boolean value.
|
|
84
|
+
* @param {any} input
|
|
85
|
+
* @returns {boolean}
|
|
86
|
+
*/
|
|
87
|
+
static sanitizeBoolean(input) {
|
|
88
|
+
if (typeof input === 'boolean') return input;
|
|
89
|
+
if (typeof input === 'string') {
|
|
90
|
+
return input.toLowerCase() === 'true' || input === '1';
|
|
91
|
+
}
|
|
92
|
+
return Boolean(input);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Sanitize an object by recursively sanitizing all string values.
|
|
97
|
+
* @param {object} obj
|
|
98
|
+
* @param {object} [options]
|
|
99
|
+
* @returns {object}
|
|
100
|
+
*/
|
|
101
|
+
static sanitizeObject(obj, options = {}) {
|
|
102
|
+
if (obj === null || typeof obj !== 'object') return obj;
|
|
103
|
+
|
|
104
|
+
if (Array.isArray(obj)) {
|
|
105
|
+
return obj.map(item => this.sanitizeObject(item, options));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const sanitized = {};
|
|
109
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
110
|
+
if (typeof value === 'string') {
|
|
111
|
+
sanitized[key] = this.sanitize(value, options);
|
|
112
|
+
} else if (typeof value === 'object') {
|
|
113
|
+
sanitized[key] = this.sanitizeObject(value, options);
|
|
114
|
+
} else {
|
|
115
|
+
sanitized[key] = value;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return sanitized;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Validate and sanitize a Discord ID.
|
|
124
|
+
* @param {string} input
|
|
125
|
+
* @returns {string|null}
|
|
126
|
+
*/
|
|
127
|
+
static sanitizeDiscordId(input) {
|
|
128
|
+
if (typeof input !== 'string') return null;
|
|
129
|
+
|
|
130
|
+
// Discord IDs are numeric strings, typically 17-19 digits
|
|
131
|
+
const match = input.match(/^\d{17,19}$/);
|
|
132
|
+
return match ? match[0] : null;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Validate and sanitize a URL.
|
|
137
|
+
* @param {string} input
|
|
138
|
+
* @param {object} [options]
|
|
139
|
+
* @param {string[]} [options.allowedProtocols=['https', 'http']]
|
|
140
|
+
* @returns {string|null}
|
|
141
|
+
*/
|
|
142
|
+
static sanitizeURL(input, options = {}) {
|
|
143
|
+
if (typeof input !== 'string') return null;
|
|
144
|
+
|
|
145
|
+
const { allowedProtocols = ['https', 'http'] } = options;
|
|
146
|
+
|
|
147
|
+
try {
|
|
148
|
+
const url = new URL(input);
|
|
149
|
+
|
|
150
|
+
if (!allowedProtocols.includes(url.protocol.replace(':', ''))) {
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Remove javascript: and data: protocols
|
|
155
|
+
if (url.protocol === 'javascript:' || url.protocol === 'data:') {
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return url.toString();
|
|
160
|
+
} catch {
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Escape special characters for regex.
|
|
167
|
+
* @param {string} string
|
|
168
|
+
* @returns {string}
|
|
169
|
+
*/
|
|
170
|
+
static escapeRegex(string) {
|
|
171
|
+
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Validate input against a whitelist of allowed values.
|
|
176
|
+
* @param {any} input
|
|
177
|
+
* @param {any[]} allowedValues
|
|
178
|
+
* @returns {any|null}
|
|
179
|
+
*/
|
|
180
|
+
static validateWhitelist(input, allowedValues) {
|
|
181
|
+
return allowedValues.includes(input) ? input : null;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
module.exports = InputSanitizer;
|