mastercontroller 1.3.13 → 1.3.15
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/MasterAction.js +302 -62
- package/MasterActionFilters.js +556 -82
- package/MasterControl.js +77 -44
- package/MasterCors.js +61 -19
- package/MasterPipeline.js +29 -6
- package/MasterRequest.js +579 -102
- package/MasterRouter.js +446 -75
- package/MasterSocket.js +380 -15
- package/MasterTemp.js +292 -10
- package/MasterTimeout.js +420 -64
- package/MasterTools.js +478 -77
- package/README.md +505 -0
- package/package.json +1 -1
- package/.claude/settings.local.json +0 -29
- package/.github/workflows/ci.yml +0 -317
- package/PERFORMANCE_SECURITY_AUDIT.md +0 -677
- package/SENIOR_ENGINEER_AUDIT.md +0 -2477
- package/VERIFICATION_CHECKLIST.md +0 -726
- package/log/mastercontroller.log +0 -2
- package/test-json-empty-body.js +0 -76
- package/test-raw-body-preservation.js +0 -128
- package/test-v1.3.4-fixes.js +0 -129
package/MasterTools.js
CHANGED
|
@@ -1,19 +1,67 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
/**
|
|
2
|
+
* MasterTools - Utility toolkit for MasterController
|
|
3
|
+
*
|
|
4
|
+
* Provides essential utilities:
|
|
5
|
+
* - String manipulation (case conversion, path parsing)
|
|
6
|
+
* - Cryptography (AES-256 encryption, secure key generation)
|
|
7
|
+
* - File conversion (base64, Buffer, streaming)
|
|
8
|
+
* - Object utilities (merging, type checking)
|
|
9
|
+
* - Random ID generation
|
|
10
|
+
*
|
|
11
|
+
* @version 1.0.0 - FAANG-level refactor with security hardening
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const crypto = require('crypto');
|
|
15
|
+
const { logger } = require('./error/MasterErrorLogger');
|
|
16
|
+
|
|
17
|
+
// Configuration Constants
|
|
18
|
+
const CRYPTO_CONFIG = {
|
|
19
|
+
ALGORITHM: 'aes-256-cbc',
|
|
20
|
+
IV_SIZE: 16, // 16 bytes for AES
|
|
21
|
+
KEY_SIZE: 256, // 256-bit key
|
|
22
|
+
HASH_ALGORITHM: 'sha256',
|
|
23
|
+
VALID_HASH_ALGORITHMS: ['sha256', 'sha512', 'sha384', 'md5', 'sha1']
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const FILE_CONFIG = {
|
|
27
|
+
MAX_FILE_SIZE: 10 * 1024 * 1024, // 10MB default
|
|
28
|
+
STREAM_THRESHOLD: 10 * 1024 * 1024, // Use streaming for files > 10MB
|
|
29
|
+
MAX_PATH_LENGTH: 4096, // Maximum file path length
|
|
30
|
+
CHUNK_SIZE: 64 * 1024 // 64KB chunks for streaming
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const STRING_CONFIG = {
|
|
34
|
+
MAX_STRING_LENGTH: 1000000, // 1MB string limit
|
|
35
|
+
MAX_WORD_ID_LENGTH: 1000, // Maximum word ID length
|
|
36
|
+
BASE64_CHARSET: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='
|
|
37
|
+
};
|
|
3
38
|
|
|
4
39
|
class MasterTools{
|
|
5
|
-
characters =
|
|
40
|
+
characters = STRING_CONFIG.BASE64_CHARSET;
|
|
6
41
|
|
|
42
|
+
/**
|
|
43
|
+
* Check if value is a plain object literal (not Array, Date, etc.)
|
|
44
|
+
*
|
|
45
|
+
* @param {*} _obj - Value to check
|
|
46
|
+
* @returns {Boolean} True if plain object literal, false otherwise
|
|
47
|
+
*
|
|
48
|
+
* @example
|
|
49
|
+
* isObjLiteral({}) // true
|
|
50
|
+
* isObjLiteral({ a: 1 }) // true
|
|
51
|
+
* isObjLiteral([]) // false
|
|
52
|
+
* isObjLiteral(new Date()) // false
|
|
53
|
+
* isObjLiteral(null) // false
|
|
54
|
+
*/
|
|
7
55
|
isObjLiteral(_obj) {
|
|
8
|
-
|
|
9
|
-
return (
|
|
10
|
-
false :
|
|
56
|
+
let _test = _obj;
|
|
57
|
+
return (typeof _obj !== 'object' || _obj === null ?
|
|
58
|
+
false :
|
|
11
59
|
(
|
|
12
60
|
(function () {
|
|
13
|
-
while (
|
|
14
|
-
if (
|
|
61
|
+
while (true) {
|
|
62
|
+
if (Object.getPrototypeOf(_test = Object.getPrototypeOf(_test)) === null) {
|
|
15
63
|
break;
|
|
16
|
-
}
|
|
64
|
+
}
|
|
17
65
|
}
|
|
18
66
|
return Object.getPrototypeOf(_obj) === _test;
|
|
19
67
|
})()
|
|
@@ -21,84 +69,274 @@ class MasterTools{
|
|
|
21
69
|
);
|
|
22
70
|
}
|
|
23
71
|
|
|
24
|
-
|
|
72
|
+
/**
|
|
73
|
+
* Remove sections from the end of a delimited string
|
|
74
|
+
*
|
|
75
|
+
* @param {String} string - Input string to process
|
|
76
|
+
* @param {Number} amount - Number of sections to remove from end
|
|
77
|
+
* @param {String} [type='\\'] - Delimiter character (default: backslash)
|
|
78
|
+
* @returns {String} String with sections removed
|
|
79
|
+
* @throws {TypeError} If string is not a string
|
|
80
|
+
* @throws {Error} If amount is negative
|
|
81
|
+
*
|
|
82
|
+
* @example
|
|
83
|
+
* removeBackwardSlashSection('a\\b\\c\\d', 2, '\\') // 'a\\b'
|
|
84
|
+
* removeBackwardSlashSection('a/b/c/d', 1, '/') // 'a/b/c'
|
|
85
|
+
*/
|
|
25
86
|
removeBackwardSlashSection(string, amount, type){
|
|
87
|
+
// Input validation
|
|
88
|
+
if (typeof string !== 'string') {
|
|
89
|
+
throw new TypeError('Input must be a string');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (typeof amount !== 'number' || amount < 0) {
|
|
93
|
+
throw new Error('Amount must be a non-negative number');
|
|
94
|
+
}
|
|
95
|
+
|
|
26
96
|
type = type === undefined ? "\\" : type;
|
|
27
|
-
|
|
28
|
-
for(
|
|
97
|
+
const stringArray = string.split(type);
|
|
98
|
+
for(let i = 0; i < amount; i++){
|
|
29
99
|
stringArray.pop();
|
|
30
100
|
}
|
|
31
101
|
return stringArray.join(type);
|
|
32
102
|
}
|
|
33
103
|
|
|
34
|
-
|
|
104
|
+
/**
|
|
105
|
+
* Extract sections from the end of a delimited string
|
|
106
|
+
*
|
|
107
|
+
* @param {String} string - Input string to process
|
|
108
|
+
* @param {Number} amount - Number of sections to extract from end
|
|
109
|
+
* @param {String} [type='\\'] - Delimiter character (default: backslash)
|
|
110
|
+
* @returns {String} Extracted sections joined by delimiter
|
|
111
|
+
* @throws {TypeError} If string is not a string
|
|
112
|
+
* @throws {Error} If amount is negative
|
|
113
|
+
*
|
|
114
|
+
* @example
|
|
115
|
+
* getBackSlashBySection('a\\b\\c\\d', 2, '\\') // 'c\\d'
|
|
116
|
+
* getBackSlashBySection('a/b/c/d', 1, '/') // 'd'
|
|
117
|
+
*/
|
|
35
118
|
getBackSlashBySection(string, amount, type){
|
|
119
|
+
// Input validation
|
|
120
|
+
if (typeof string !== 'string') {
|
|
121
|
+
throw new TypeError('Input must be a string');
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (typeof amount !== 'number' || amount < 0) {
|
|
125
|
+
throw new Error('Amount must be a non-negative number');
|
|
126
|
+
}
|
|
127
|
+
|
|
36
128
|
type = type === undefined ? "\\" : type;
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
for(
|
|
129
|
+
const stringArray = string.split(type);
|
|
130
|
+
const newStringArray = [];
|
|
131
|
+
for(let i = 0; i < amount; i++){
|
|
40
132
|
newStringArray.unshift(stringArray.pop());
|
|
41
133
|
}
|
|
42
134
|
return newStringArray.join(type);
|
|
43
135
|
}
|
|
44
136
|
|
|
137
|
+
/**
|
|
138
|
+
* Capitalize first letter of string
|
|
139
|
+
*
|
|
140
|
+
* @param {String} string - Input string
|
|
141
|
+
* @returns {String} String with first letter capitalized
|
|
142
|
+
* @throws {TypeError} If string is not a string
|
|
143
|
+
* @throws {Error} If string is empty
|
|
144
|
+
*
|
|
145
|
+
* @example
|
|
146
|
+
* firstLetterUppercase('hello') // 'Hello'
|
|
147
|
+
* firstLetterUppercase('world') // 'World'
|
|
148
|
+
*/
|
|
45
149
|
firstLetterUppercase(string){
|
|
150
|
+
if (typeof string !== 'string') {
|
|
151
|
+
throw new TypeError('Input must be a string');
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (string.length === 0) {
|
|
155
|
+
throw new Error('String cannot be empty');
|
|
156
|
+
}
|
|
157
|
+
|
|
46
158
|
return string.charAt(0).toUpperCase() + string.slice(1);
|
|
47
|
-
}
|
|
48
|
-
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Lowercase first letter of string
|
|
163
|
+
*
|
|
164
|
+
* @param {String} string - Input string
|
|
165
|
+
* @returns {String} String with first letter lowercased
|
|
166
|
+
* @throws {TypeError} If string is not a string
|
|
167
|
+
* @throws {Error} If string is empty
|
|
168
|
+
*
|
|
169
|
+
* @example
|
|
170
|
+
* firstLetterlowercase('Hello') // 'hello'
|
|
171
|
+
* firstLetterlowercase('World') // 'world'
|
|
172
|
+
*/
|
|
49
173
|
firstLetterlowercase(string){
|
|
50
|
-
|
|
51
|
-
|
|
174
|
+
if (typeof string !== 'string') {
|
|
175
|
+
throw new TypeError('Input must be a string');
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (string.length === 0) {
|
|
179
|
+
throw new Error('String cannot be empty');
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return string.charAt(0).toLowerCase() + string.slice(1);
|
|
183
|
+
}
|
|
52
184
|
|
|
185
|
+
/**
|
|
186
|
+
* Encrypt data using AES-256-CBC with random IV
|
|
187
|
+
*
|
|
188
|
+
* @param {*} payload - Data to encrypt (will be converted to string)
|
|
189
|
+
* @param {String} secret - Encryption secret/password
|
|
190
|
+
* @returns {String} Encrypted data with IV prepended (format: "iv:encryptedData")
|
|
191
|
+
* @throws {TypeError} If secret is not provided
|
|
192
|
+
* @throws {Error} If encryption fails
|
|
193
|
+
*
|
|
194
|
+
* @example
|
|
195
|
+
* const encrypted = encrypt('sensitive data', 'mySecretKey123');
|
|
196
|
+
* // Returns: "a1b2c3...iv...:d4e5f6...encrypted..."
|
|
197
|
+
*/
|
|
53
198
|
encrypt(payload, secret){
|
|
54
|
-
|
|
55
|
-
|
|
199
|
+
try {
|
|
200
|
+
// Input validation
|
|
201
|
+
if (!secret || typeof secret !== 'string') {
|
|
202
|
+
throw new TypeError('Secret must be a non-empty string');
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (secret.length < 8) {
|
|
206
|
+
logger.warn({
|
|
207
|
+
code: 'MC_CRYPTO_WEAK_SECRET',
|
|
208
|
+
message: 'Encryption secret is shorter than recommended (8+ characters)'
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Generate random IV (16 bytes for AES)
|
|
213
|
+
const iv = crypto.randomBytes(CRYPTO_CONFIG.IV_SIZE);
|
|
56
214
|
|
|
57
|
-
|
|
58
|
-
|
|
215
|
+
// Create 256-bit key from secret
|
|
216
|
+
const key = crypto.createHash(CRYPTO_CONFIG.HASH_ALGORITHM).update(String(secret)).digest();
|
|
59
217
|
|
|
60
|
-
|
|
61
|
-
|
|
218
|
+
// Create cipher with AES-256-CBC
|
|
219
|
+
const cipher = crypto.createCipheriv(CRYPTO_CONFIG.ALGORITHM, key, iv);
|
|
62
220
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
221
|
+
// Encrypt payload
|
|
222
|
+
let encrypted = cipher.update(String(payload), 'utf8', 'hex');
|
|
223
|
+
encrypted += cipher.final('hex');
|
|
66
224
|
|
|
67
|
-
|
|
68
|
-
|
|
225
|
+
// Prepend IV to encrypted data (IV is not secret, needed for decryption)
|
|
226
|
+
return iv.toString('hex') + ':' + encrypted;
|
|
227
|
+
|
|
228
|
+
} catch (error) {
|
|
229
|
+
logger.error({
|
|
230
|
+
code: 'MC_CRYPTO_ENCRYPT_ERROR',
|
|
231
|
+
message: 'Encryption failed',
|
|
232
|
+
error: error.message
|
|
233
|
+
});
|
|
234
|
+
throw new Error(`Encryption failed: ${error.message}`);
|
|
235
|
+
}
|
|
69
236
|
}
|
|
70
237
|
|
|
238
|
+
/**
|
|
239
|
+
* Decrypt AES-256-CBC encrypted data
|
|
240
|
+
*
|
|
241
|
+
* @param {String} encryption - Encrypted data with IV (format: "iv:encryptedData")
|
|
242
|
+
* @param {String} secret - Decryption secret/password (must match encryption secret)
|
|
243
|
+
* @returns {String} Decrypted plaintext data
|
|
244
|
+
* @throws {TypeError} If inputs are invalid
|
|
245
|
+
* @throws {Error} If decryption fails
|
|
246
|
+
*
|
|
247
|
+
* @example
|
|
248
|
+
* const encrypted = encrypt('sensitive data', 'mySecretKey123');
|
|
249
|
+
* const decrypted = decrypt(encrypted, 'mySecretKey123');
|
|
250
|
+
* // Returns: "sensitive data"
|
|
251
|
+
*/
|
|
71
252
|
decrypt(encryption, secret){
|
|
72
253
|
try {
|
|
254
|
+
// Input validation
|
|
255
|
+
if (!encryption || typeof encryption !== 'string') {
|
|
256
|
+
throw new TypeError('Encrypted data must be a non-empty string');
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (!secret || typeof secret !== 'string') {
|
|
260
|
+
throw new TypeError('Secret must be a non-empty string');
|
|
261
|
+
}
|
|
262
|
+
|
|
73
263
|
// Split IV and encrypted data
|
|
74
264
|
const parts = encryption.split(':');
|
|
75
265
|
if (parts.length !== 2) {
|
|
76
|
-
throw new Error('Invalid encrypted data format');
|
|
266
|
+
throw new Error('Invalid encrypted data format (expected "iv:encryptedData")');
|
|
77
267
|
}
|
|
78
268
|
|
|
79
269
|
const iv = Buffer.from(parts[0], 'hex');
|
|
80
270
|
const encryptedData = parts[1];
|
|
81
271
|
|
|
272
|
+
// Validate IV size
|
|
273
|
+
if (iv.length !== CRYPTO_CONFIG.IV_SIZE) {
|
|
274
|
+
throw new Error(`Invalid IV size (expected ${CRYPTO_CONFIG.IV_SIZE} bytes, got ${iv.length})`);
|
|
275
|
+
}
|
|
276
|
+
|
|
82
277
|
// Create 256-bit key from secret
|
|
83
|
-
const key = crypto.createHash(
|
|
278
|
+
const key = crypto.createHash(CRYPTO_CONFIG.HASH_ALGORITHM).update(String(secret)).digest();
|
|
84
279
|
|
|
85
280
|
// Create decipher
|
|
86
|
-
const decipher = crypto.createDecipheriv(
|
|
281
|
+
const decipher = crypto.createDecipheriv(CRYPTO_CONFIG.ALGORITHM, key, iv);
|
|
87
282
|
|
|
88
283
|
// Decrypt
|
|
89
284
|
let decrypted = decipher.update(encryptedData, 'hex', 'utf8');
|
|
90
285
|
decrypted += decipher.final('utf8');
|
|
91
286
|
|
|
92
287
|
return decrypted;
|
|
288
|
+
|
|
93
289
|
} catch (error) {
|
|
94
|
-
|
|
290
|
+
logger.error({
|
|
291
|
+
code: 'MC_CRYPTO_DECRYPT_ERROR',
|
|
292
|
+
message: 'Decryption failed',
|
|
293
|
+
error: error.message
|
|
294
|
+
});
|
|
295
|
+
throw new Error(`Decryption failed: ${error.message}`);
|
|
95
296
|
}
|
|
96
297
|
}
|
|
97
298
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
299
|
+
/**
|
|
300
|
+
* Generate cryptographically secure random key
|
|
301
|
+
*
|
|
302
|
+
* SECURITY FIX: Now uses crypto.randomBytes() instead of Math.random()
|
|
303
|
+
* for cryptographically secure random number generation
|
|
304
|
+
*
|
|
305
|
+
* @param {String} [hash='sha256'] - Hash algorithm (sha256, sha512, sha384, md5, sha1)
|
|
306
|
+
* @returns {String} Hex-encoded random key
|
|
307
|
+
* @throws {Error} If hash algorithm is invalid
|
|
308
|
+
*
|
|
309
|
+
* @example
|
|
310
|
+
* const key256 = generateRandomKey('sha256'); // 64 character hex string
|
|
311
|
+
* const key512 = generateRandomKey('sha512'); // 128 character hex string
|
|
312
|
+
*/
|
|
313
|
+
generateRandomKey(hash = CRYPTO_CONFIG.HASH_ALGORITHM){
|
|
314
|
+
// Input validation
|
|
315
|
+
if (!CRYPTO_CONFIG.VALID_HASH_ALGORITHMS.includes(hash)) {
|
|
316
|
+
throw new Error(
|
|
317
|
+
`Invalid hash algorithm: ${hash}. ` +
|
|
318
|
+
`Valid options: ${CRYPTO_CONFIG.VALID_HASH_ALGORITHMS.join(', ')}`
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
try {
|
|
323
|
+
// CRITICAL SECURITY FIX: Use crypto.randomBytes() instead of Math.random()
|
|
324
|
+
// Math.random() is NOT cryptographically secure and must never be used for keys
|
|
325
|
+
const randomBytes = crypto.randomBytes(32); // 32 bytes = 256 bits of entropy
|
|
326
|
+
|
|
327
|
+
const sha = crypto.createHash(hash);
|
|
328
|
+
sha.update(randomBytes);
|
|
329
|
+
return sha.digest('hex');
|
|
330
|
+
|
|
331
|
+
} catch (error) {
|
|
332
|
+
logger.error({
|
|
333
|
+
code: 'MC_CRYPTO_KEY_GENERATION_ERROR',
|
|
334
|
+
message: 'Failed to generate random key',
|
|
335
|
+
hash,
|
|
336
|
+
error: error.message
|
|
337
|
+
});
|
|
338
|
+
throw new Error(`Key generation failed: ${error.message}`);
|
|
339
|
+
}
|
|
102
340
|
}
|
|
103
341
|
|
|
104
342
|
/**
|
|
@@ -117,9 +355,14 @@ class MasterTools{
|
|
|
117
355
|
* const base64 = tools.fileToBase64('/path/to/file.jpg');
|
|
118
356
|
*/
|
|
119
357
|
base64(){
|
|
120
|
-
|
|
358
|
+
logger.warn({
|
|
359
|
+
code: 'MC_TOOLS_DEPRECATED_BASE64',
|
|
360
|
+
message: 'MasterTools.base64() is deprecated and only works for TEXT strings, not binary files',
|
|
361
|
+
recommendation: 'Use Buffer.toString("base64") or tools.fileToBase64() instead',
|
|
362
|
+
removal: 'This method will be removed in v2.0'
|
|
363
|
+
});
|
|
121
364
|
|
|
122
|
-
|
|
365
|
+
const $that = this;
|
|
123
366
|
return {
|
|
124
367
|
encode: function(string){
|
|
125
368
|
|
|
@@ -547,75 +790,233 @@ class MasterTools{
|
|
|
547
790
|
return mimeTypes[ext] || 'application/octet-stream';
|
|
548
791
|
}
|
|
549
792
|
|
|
793
|
+
/**
|
|
794
|
+
* Combine object or array of objects into target object
|
|
795
|
+
*
|
|
796
|
+
* @param {Object|Array} data - Source object or array of objects
|
|
797
|
+
* @param {Object} objParams - Target object to merge into
|
|
798
|
+
* @returns {Object} Combined object
|
|
799
|
+
* @throws {TypeError} If objParams is not an object
|
|
800
|
+
*
|
|
801
|
+
* @example
|
|
802
|
+
* combineObjandArray({ a: 1, b: 2 }, {}) // { a: 1, b: 2 }
|
|
803
|
+
* combineObjandArray([{ a: 1 }, { b: 2 }], {}) // { a: 1, b: 2 }
|
|
804
|
+
*/
|
|
550
805
|
combineObjandArray(data, objParams){
|
|
806
|
+
// Input validation
|
|
807
|
+
if (!objParams || typeof objParams !== 'object') {
|
|
808
|
+
throw new TypeError('objParams must be an object');
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
if (!data) {
|
|
812
|
+
return objParams;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
// Prototype pollution protection
|
|
816
|
+
const isSafeKey = (key) => {
|
|
817
|
+
return key !== '__proto__' && key !== 'constructor' && key !== 'prototype';
|
|
818
|
+
};
|
|
551
819
|
|
|
552
820
|
if(Array.isArray(data) === false){
|
|
553
821
|
// if data is object
|
|
554
|
-
for (
|
|
555
|
-
if (
|
|
822
|
+
for (const key in data) {
|
|
823
|
+
if (Object.prototype.hasOwnProperty.call(data, key) && isSafeKey(key)) {
|
|
556
824
|
objParams[key] = data[key];
|
|
557
825
|
}
|
|
558
|
-
}
|
|
826
|
+
}
|
|
559
827
|
}
|
|
560
828
|
else{
|
|
561
|
-
for(
|
|
829
|
+
for(let y = 0; y < data.length; y++){
|
|
562
830
|
// inside array we have an object
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
831
|
+
if (data[y] && typeof data[y] === 'object') {
|
|
832
|
+
for (const key in data[y]) {
|
|
833
|
+
if (Object.prototype.hasOwnProperty.call(data[y], key) && isSafeKey(key)) {
|
|
834
|
+
objParams[key] = data[y][key];
|
|
835
|
+
}
|
|
566
836
|
}
|
|
567
|
-
}
|
|
837
|
+
}
|
|
568
838
|
}
|
|
569
|
-
}
|
|
570
|
-
|
|
839
|
+
}
|
|
840
|
+
|
|
571
841
|
return objParams;
|
|
572
|
-
}
|
|
842
|
+
}
|
|
573
843
|
|
|
844
|
+
/**
|
|
845
|
+
* Check if value is a function
|
|
846
|
+
*
|
|
847
|
+
* @param {*} obj - Value to check
|
|
848
|
+
* @returns {Boolean} True if value is a function
|
|
849
|
+
*
|
|
850
|
+
* @example
|
|
851
|
+
* isFunction(() => {}) // true
|
|
852
|
+
* isFunction(function() {}) // true
|
|
853
|
+
* isFunction({}) // false
|
|
854
|
+
*/
|
|
574
855
|
isFunction(obj) {
|
|
575
856
|
return !!(obj && obj.constructor && obj.call && obj.apply);
|
|
576
|
-
}
|
|
857
|
+
}
|
|
577
858
|
|
|
859
|
+
/**
|
|
860
|
+
* Merge source object properties into target object
|
|
861
|
+
*
|
|
862
|
+
* @param {Object} obj - Target object
|
|
863
|
+
* @param {Object} src - Source object to merge from
|
|
864
|
+
* @returns {Object} Merged object
|
|
865
|
+
* @throws {TypeError} If src is not an object
|
|
866
|
+
*
|
|
867
|
+
* @example
|
|
868
|
+
* combineObjects({ a: 1 }, { b: 2 }) // { a: 1, b: 2 }
|
|
869
|
+
*/
|
|
578
870
|
combineObjects(obj, src) {
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
};
|
|
583
|
-
return obj;
|
|
871
|
+
// Input validation
|
|
872
|
+
if (!src || typeof src !== 'object') {
|
|
873
|
+
throw new TypeError('Source must be an object');
|
|
584
874
|
}
|
|
585
|
-
|
|
586
|
-
|
|
875
|
+
|
|
876
|
+
if(!obj || typeof obj !== 'object'){
|
|
877
|
+
return {};
|
|
587
878
|
}
|
|
588
879
|
|
|
589
|
-
|
|
880
|
+
// Prototype pollution protection
|
|
881
|
+
const isSafeKey = (key) => {
|
|
882
|
+
return key !== '__proto__' && key !== 'constructor' && key !== 'prototype';
|
|
883
|
+
};
|
|
590
884
|
|
|
885
|
+
for(const i in src){
|
|
886
|
+
if (Object.prototype.hasOwnProperty.call(src, i) && isSafeKey(i)) {
|
|
887
|
+
obj[i] = src[i];
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
return obj;
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
/**
|
|
894
|
+
* Generate random alphanumeric ID
|
|
895
|
+
*
|
|
896
|
+
* WARNING: Uses Math.random() which is NOT cryptographically secure.
|
|
897
|
+
* For secure keys, use generateRandomKey() instead.
|
|
898
|
+
*
|
|
899
|
+
* @param {Number} length - Length of ID to generate
|
|
900
|
+
* @returns {String} Random alphanumeric string
|
|
901
|
+
* @throws {TypeError} If length is not a number
|
|
902
|
+
* @throws {Error} If length is invalid
|
|
903
|
+
*
|
|
904
|
+
* @example
|
|
905
|
+
* makeWordId(8) // 'aBcDeFgH'
|
|
906
|
+
* makeWordId(16) // 'xYzAbCdEfGhIjKlM'
|
|
907
|
+
*/
|
|
591
908
|
makeWordId(length) {
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
909
|
+
// Input validation
|
|
910
|
+
if (typeof length !== 'number' || isNaN(length)) {
|
|
911
|
+
throw new TypeError('Length must be a number');
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
if (length <= 0 || length > STRING_CONFIG.MAX_WORD_ID_LENGTH) {
|
|
915
|
+
throw new Error(
|
|
916
|
+
`Length must be between 1 and ${STRING_CONFIG.MAX_WORD_ID_LENGTH}`
|
|
917
|
+
);
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
let result = '';
|
|
921
|
+
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
|
|
922
|
+
const charactersLength = characters.length;
|
|
923
|
+
for (let i = 0; i < length; i++) {
|
|
596
924
|
result += characters.charAt(Math.floor(Math.random() * charactersLength));
|
|
597
925
|
}
|
|
598
926
|
return result;
|
|
599
|
-
}
|
|
927
|
+
}
|
|
600
928
|
|
|
929
|
+
/**
|
|
930
|
+
* Merge source object properties into target object prototype
|
|
931
|
+
*
|
|
932
|
+
* WARNING: Modifying prototypes can be dangerous. Use with caution.
|
|
933
|
+
*
|
|
934
|
+
* @param {Function} obj - Constructor function with prototype to modify
|
|
935
|
+
* @param {Object} src - Source object with properties to add to prototype
|
|
936
|
+
* @returns {Function} Modified constructor function
|
|
937
|
+
* @throws {TypeError} If obj is not a function or src is not an object
|
|
938
|
+
*
|
|
939
|
+
* @example
|
|
940
|
+
* function MyClass() {}
|
|
941
|
+
* combineObjectPrototype(MyClass, { method1: function() {} })
|
|
942
|
+
* // MyClass.prototype.method1 is now available
|
|
943
|
+
*/
|
|
601
944
|
combineObjectPrototype(obj, src) {
|
|
602
|
-
|
|
603
|
-
|
|
945
|
+
// Input validation
|
|
946
|
+
if (typeof obj !== 'function') {
|
|
947
|
+
throw new TypeError('Object must be a constructor function');
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
if (!src || typeof src !== 'object') {
|
|
951
|
+
throw new TypeError('Source must be an object');
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
// Prototype pollution protection
|
|
955
|
+
const isSafeKey = (key) => {
|
|
956
|
+
return key !== '__proto__' && key !== 'constructor' && key !== 'prototype';
|
|
604
957
|
};
|
|
958
|
+
|
|
959
|
+
for(const i in src){
|
|
960
|
+
if (Object.prototype.hasOwnProperty.call(src, i) && isSafeKey(i)) {
|
|
961
|
+
obj.prototype[i] = src[i];
|
|
962
|
+
}
|
|
963
|
+
}
|
|
605
964
|
return obj;
|
|
606
|
-
}
|
|
965
|
+
}
|
|
607
966
|
|
|
967
|
+
/**
|
|
968
|
+
* Convert array path to nested object structure
|
|
969
|
+
*
|
|
970
|
+
* @param {Object} obj - Target object to modify
|
|
971
|
+
* @param {Array} keyPath - Array of keys representing path (e.g., ['a', 'b', 'c'])
|
|
972
|
+
* @param {*} value - Value to set at the path
|
|
973
|
+
* @returns {void}
|
|
974
|
+
* @throws {TypeError} If obj is not an object or keyPath is not an array
|
|
975
|
+
*
|
|
976
|
+
* @example
|
|
977
|
+
* const obj = {};
|
|
978
|
+
* convertArrayToObject(obj, ['user', 'name'], 'John')
|
|
979
|
+
* // obj is now: { user: { name: 'John' } }
|
|
980
|
+
*/
|
|
608
981
|
convertArrayToObject(obj, keyPath, value) {
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
982
|
+
// Input validation
|
|
983
|
+
if (!obj || typeof obj !== 'object') {
|
|
984
|
+
throw new TypeError('Object must be an object');
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
if (!Array.isArray(keyPath) || keyPath.length === 0) {
|
|
988
|
+
throw new TypeError('keyPath must be a non-empty array');
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
// Prototype pollution protection
|
|
992
|
+
const isSafeKey = (key) => {
|
|
993
|
+
return key !== '__proto__' && key !== 'constructor' && key !== 'prototype';
|
|
994
|
+
};
|
|
995
|
+
|
|
996
|
+
let key = null;
|
|
997
|
+
const lastKeyIndex = keyPath.length - 1;
|
|
998
|
+
|
|
999
|
+
for (let i = 0; i < lastKeyIndex; ++i) {
|
|
1000
|
+
key = keyPath[i];
|
|
1001
|
+
|
|
1002
|
+
// Security check
|
|
1003
|
+
if (!isSafeKey(key)) {
|
|
1004
|
+
throw new Error(`Unsafe key in path: ${key}`);
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
if (!(key in obj)) {
|
|
1008
|
+
obj[key] = {};
|
|
1009
|
+
}
|
|
1010
|
+
obj = obj[key];
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
// Security check for final key
|
|
1014
|
+
if (!isSafeKey(keyPath[lastKeyIndex])) {
|
|
1015
|
+
throw new Error(`Unsafe key in path: ${keyPath[lastKeyIndex]}`);
|
|
616
1016
|
}
|
|
1017
|
+
|
|
617
1018
|
obj[keyPath[lastKeyIndex]] = value;
|
|
618
|
-
|
|
1019
|
+
}
|
|
619
1020
|
|
|
620
1021
|
|
|
621
1022
|
}
|