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/MasterTools.js CHANGED
@@ -1,19 +1,67 @@
1
- // version 0.0.2
2
- var crypto = require('crypto');
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 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
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
- var _test = _obj;
9
- return ( typeof _obj !== 'object' || _obj === null ?
10
- false :
56
+ let _test = _obj;
57
+ return (typeof _obj !== 'object' || _obj === null ?
58
+ false :
11
59
  (
12
60
  (function () {
13
- while (!false) {
14
- if ( Object.getPrototypeOf( _test = Object.getPrototypeOf(_test) ) === null) {
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
- // this will remove everthing from back slash amount
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
- var stringArray = string.split(type);
28
- for(var i = 0; i < amount; i++){
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
- // return only the number of back slash amount
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
- var stringArray = string.split(type);
38
- var newStringArray = [];
39
- for(var i = 0; i < amount; i++){
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
- return string.charAt(0).toLowerCase() + string.slice(1);
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
- // Generate random IV (16 bytes for AES)
55
- const iv = crypto.randomBytes(16);
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
- // Create 256-bit key from secret
58
- const key = crypto.createHash('sha256').update(String(secret)).digest();
215
+ // Create 256-bit key from secret
216
+ const key = crypto.createHash(CRYPTO_CONFIG.HASH_ALGORITHM).update(String(secret)).digest();
59
217
 
60
- // Create cipher with AES-256-CBC
61
- const cipher = crypto.createCipheriv('aes-256-cbc', key, iv);
218
+ // Create cipher with AES-256-CBC
219
+ const cipher = crypto.createCipheriv(CRYPTO_CONFIG.ALGORITHM, key, iv);
62
220
 
63
- // Encrypt payload
64
- let encrypted = cipher.update(String(payload), 'utf8', 'hex');
65
- encrypted += cipher.final('hex');
221
+ // Encrypt payload
222
+ let encrypted = cipher.update(String(payload), 'utf8', 'hex');
223
+ encrypted += cipher.final('hex');
66
224
 
67
- // Prepend IV to encrypted data (IV is not secret, needed for decryption)
68
- return iv.toString('hex') + ':' + encrypted;
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('sha256').update(String(secret)).digest();
278
+ const key = crypto.createHash(CRYPTO_CONFIG.HASH_ALGORITHM).update(String(secret)).digest();
84
279
 
85
280
  // Create decipher
86
- const decipher = crypto.createDecipheriv('aes-256-cbc', key, iv);
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
- throw new Error('Decryption failed: ' + error.message);
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
- generateRandomKey(hash){
99
- var sha = crypto.createHash(hash);
100
- sha.update(Math.random().toString());
101
- return sha.digest('hex');
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
- console.warn('[DEPRECATED] MasterTools.base64() only works for TEXT strings, not binary files. Use Buffer.toString("base64") or tools.fileToBase64() instead. This method will be removed in v2.0.');
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
- var $that = this;
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 (var key in data) {
555
- if (data.hasOwnProperty(key)) {
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(var y = 0; y < data.length; y++){
829
+ for(let y = 0; y < data.length; y++){
562
830
  // inside array we have an object
563
- for (var key in data[y]) {
564
- if (data[y].hasOwnProperty(key)) {
565
- objParams[key] = data[y][key];
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
- if(obj){
580
- for(var i in src){
581
- obj[i] = src[i];
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
- else{
586
- return {}
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
- var result = '';
593
- var characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
594
- var charactersLength = characters.length;
595
- for ( var i = 0; i < length; i++ ) {
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
- for(var i in src){
603
- obj.prototype[i] = src[i];
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
- var key = null;
610
- var lastKeyIndex = keyPath.length-1;
611
- for (var i = 0; i < lastKeyIndex; ++ i) {
612
- key = keyPath[i];
613
- if (!(key in obj))
614
- obj[key] = {}
615
- obj = obj[key];
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
  }