UNPKG

security-words-picker

Version:

A robust and versatile package for selecting, filtering, and managing word lists with advanced security features.

332 lines (291 loc) 11.7 kB
const fs = require('fs'); const path = require('path'); /** * Retrieves a specified number of words based on optional constraints. * * @param {Object} options - An object containing optional parameters. * @param {number} amountOfWords - Number of words to retrieve. * @param {Array<string>} [customWordsArray] - (Optional) Custom array of words to use instead of words.txt or words.js. * @param {function} [customErrorHandler] - (Optional) Custom function to handle errors. * @returns {Array|string} - An array or string of words matching the specified criteria. */ function getWords(options = {}, amountOfWords, customWordsArray = null, customErrorHandler = null) { // Validate 'amountOfWords' parameter if (typeof amountOfWords !== 'number' || amountOfWords <= 0) { throw new Error("'amountOfWords' must be a positive number."); } // Load words from words.txt if available, else use words.js, else use customWordsArray or throw error let defaultWords = []; const wordsTxtPath = path.join(__dirname, 'words', 'words.txt'); const wordsJsPath = path.join(__dirname, 'words', 'words.js'); try { if (customWordsArray && Array.isArray(customWordsArray)) { defaultWords = customWordsArray; console.log(`Using customWordsArray with ${defaultWords.length} words.`); } else if (fs.existsSync(wordsTxtPath)) { const data = fs.readFileSync(wordsTxtPath, 'utf8'); // Split by newlines and remove any surrounding quotes and commas defaultWords = data.split('\n').map(line => line.trim().replace(/^["']|["']$/g, '')).filter(line => line.length > 0); console.log(`Loaded ${defaultWords.length} words from words.txt`); } else if (fs.existsSync(wordsJsPath)) { defaultWords = require(wordsJsPath); if (!Array.isArray(defaultWords)) { throw new Error('words.js must export an array of words.'); } console.log(`Loaded ${defaultWords.length} words from words.js`); } else { throw new Error('No words.txt or words.js found in the /words directory, and no customWordsArray provided.'); } } catch (err) { if (customErrorHandler && typeof customErrorHandler === 'function') { customErrorHandler(err); } else { console.error(`Error loading words: ${err.message}`); process.exit(1); } } // Destructure options with default values const { lengthMin, lengthMax, fixLength, reverse = false, asString = false, sort, caseOption, filterStartsWith, filterEndsWith, excludeSubstrings, blacklist, whitelist, excludeAmbiguous = false, pattern, phoneticDistinct = true, includeMetadata = false, history = new Set(), allowNumbers = false, allowSpecialChars = false, uniqueCharacters = false, maxRepeatLetters, excludeWordsWithRepeatingLetters = false, minConsonants, minVowels, customFilter } = options; // Early return if amountOfWords is zero if (amountOfWords === 0) return asString ? "" : []; // Initialize Sets for blacklist and whitelist for O(1) lookups const blacklistSet = blacklist ? new Set(blacklist.map(word => word.toLowerCase())) : null; const whitelistSet = whitelist ? new Set(whitelist.map(word => word.toLowerCase())) : null; // Characters considered ambiguous const ambiguousChars = /[l1I0O]/i; // Function to calculate entropy (simple estimation based on word length and uniqueness) const calculateEntropy = (word) => { if (customEntropyCalculator && typeof customEntropyCalculator === 'function') { return customEntropyCalculator(word); } // Simple entropy calculation: number of unique characters const uniqueChars = new Set(word.toLowerCase()).size; return uniqueChars * Math.log2(26); }; // Function to check phonetic distinctness (simple implementation using Soundex) const soundex = (word) => { const a = word.toLowerCase().split(''); const f = a.shift(); const codes = { a: '', e: '', i: '', o: '', u: '', b: '1', f: '1', p: '1', v: '1', c: '2', g: '2', j: '2', k: '2', q: '2', s: '2', x: '2', z: '2', d: '3', t: '3', l: '4', m: '5', n: '5', r: '6' }; const soundexArr = [f.toUpperCase()]; let prev = ''; a.forEach(char => { const code = codes[char]; if (code && code !== prev) { soundexArr.push(code); prev = code; } }); // Pad with zeros or truncate to ensure length 4 while (soundexArr.length < 4) soundexArr.push('0'); return soundexArr.slice(0, 4).join(''); }; let phoneticMap = {}; // Start filtering let filteredWords = defaultWords.filter(word => { const lowerWord = word.toLowerCase(); // Apply whitelist: if whitelist is present, only include those words if (whitelistSet && !whitelistSet.has(lowerWord)) { return false; } // Apply blacklist if (blacklistSet && blacklistSet.has(lowerWord)) { return false; } // Apply length filters if (fixLength !== undefined && word.length !== fixLength) { return false; } if (lengthMin !== undefined && word.length < lengthMin) { return false; } if (lengthMax !== undefined && word.length > lengthMax) { return false; } // Exclude words with ambiguous characters if (excludeAmbiguous && ambiguousChars.test(word)) { return false; } // Apply filterStartsWith if (filterStartsWith && Array.isArray(filterStartsWith) && filterStartsWith.length > 0) { const startsWithMatch = filterStartsWith.some(prefix => word.toLowerCase().startsWith(prefix.toLowerCase())); if (!startsWithMatch) return false; } // Apply filterEndsWith if (filterEndsWith && Array.isArray(filterEndsWith) && filterEndsWith.length > 0) { const endsWithMatch = filterEndsWith.some(suffix => word.toLowerCase().endsWith(suffix.toLowerCase())); if (!endsWithMatch) return false; } // Apply excludeSubstrings if (excludeSubstrings && Array.isArray(excludeSubstrings) && excludeSubstrings.length > 0) { const hasExcludedSub = excludeSubstrings.some(sub => word.toLowerCase().includes(sub.toLowerCase())); if (hasExcludedSub) return false; } // Apply pattern if (pattern instanceof RegExp && !pattern.test(word)) { return false; } // Exclude words already in history to ensure uniqueness across sessions if (history.has(lowerWord)) { return false; } // Apply uniqueCharacters if (uniqueCharacters) { const uniqueChars = new Set(word.toLowerCase()).size; if (uniqueChars !== word.length) { return false; } } // Apply maxRepeatLetters if (maxRepeatLetters !== undefined && maxRepeatLetters > 0) { const letterCounts = {}; for (let char of word.toLowerCase()) { letterCounts[char] = (letterCounts[char] || 0) + 1; if (letterCounts[char] > maxRepeatLetters) { return false; } } } // Apply allowNumbers and allowSpecialChars if (!allowNumbers && /\d/.test(word)) { return false; } if (!allowSpecialChars && /[^a-zA-Z]/.test(word)) { return false; } // Apply customFilter if (customFilter && typeof customFilter === 'function') { if (!customFilter(word)) { return false; } } // All filters passed return true; }); // Apply phonetic distinctness if (phoneticDistinct) { filteredWords = filteredWords.filter(word => { const s = soundex(word); if (phoneticMap[s]) { return false; } phoneticMap[s] = true; return true; }); } // Apply weighted selection if (options.weightedSelection && typeof options.weightedSelection === 'object') { const weightedWords = []; filteredWords.forEach(word => { const weight = options.weightedSelection[word.toLowerCase()] || 1; for (let i = 0; i < weight; i++) { weightedWords.push(word); } }); filteredWords = weightedWords; } // Apply custom shuffle const shuffle = (array) => { if (options.customShuffle && typeof options.customShuffle === 'function') { options.customShuffle(array); return; } // Fisher-Yates Shuffle with simple randomness for (let i = array.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [array[i], array[j]] = [array[j], array[i]]; } }; shuffle(filteredWords); // Apply sort if required if (sort === 'asc') { filteredWords.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase())); } else if (sort === 'desc') { filteredWords.sort((a, b) => b.toLowerCase().localeCompare(a.toLowerCase())); } // Select the desired number of words let selectedWords = filteredWords.slice(0, amountOfWords); // Apply 'reverse' if needed if (reverse) { selectedWords = selectedWords.reverse(); } // Apply 'caseOption' if needed if (caseOption === 'upper') { selectedWords = selectedWords.map(word => word.toUpperCase()); } else if (caseOption === 'lower') { selectedWords = selectedWords.map(word => word.toLowerCase()); } else if (caseOption === 'capitalize') { selectedWords = selectedWords.map(word => word.charAt(0).toUpperCase() + word.slice(1)); } // Update history to include selected words selectedWords.forEach(word => history.add(word.toLowerCase())); // Return as string if required if (asString) { return selectedWords.join(', '); } // Return as array, possibly with metadata if (includeMetadata) { return selectedWords.map(word => ({ word: word, length: word.length, entropy: calculateEntropy(word), // Add more metadata as needed })); } return selectedWords; } // Helper function to calculate Scrabble score function calculateScrabbleScore(word) { const scores = { a: 1, b: 3, c: 3, d: 2, e: 1, f: 4, g: 2, h: 4, i: 1, j: 8, k: 5, l: 1, m: 3, n: 1, o: 1, p: 3, q: 10, r: 1, s: 1, t: 1, u: 1, v: 4, w: 4, x: 8, y: 4, z: 10 }; return word.toLowerCase().split('').reduce((acc, char) => acc + (scores[char] || 0), 0); } // Export the functions for use in other files module.exports = { getWords, pickWords: (options = {}) => { const { count, ...restOptions } = options; if (!count || typeof count !== 'number' || count <= 0) { throw new Error("'count' must be a positive number."); } return getWords(restOptions, count); } };