lsh-framework 3.1.23 → 3.1.25

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.
@@ -1,318 +0,0 @@
1
- /**
2
- * Input Validation Utilities
3
- * Provides secure input validation for user-facing data
4
- */
5
- /**
6
- * Common disposable email domains that should be blocked for signups.
7
- * This is a minimal list - consider using a more comprehensive service
8
- * for production use.
9
- */
10
- const DISPOSABLE_EMAIL_DOMAINS = new Set([
11
- 'mailinator.com',
12
- 'guerrillamail.com',
13
- 'guerrillamail.org',
14
- 'tempmail.com',
15
- 'temp-mail.org',
16
- '10minutemail.com',
17
- 'throwaway.email',
18
- 'fakeinbox.com',
19
- 'trashmail.com',
20
- 'yopmail.com',
21
- 'maildrop.cc',
22
- 'getnada.com',
23
- 'sharklasers.com',
24
- 'dispostable.com',
25
- 'mailnesia.com',
26
- ]);
27
- /**
28
- * RFC 5322 compliant email regex (simplified but comprehensive).
29
- * Allows most valid email formats while rejecting clearly invalid ones.
30
- */
31
- const EMAIL_REGEX = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/;
32
- /**
33
- * Maximum email length per RFC 5321
34
- */
35
- const MAX_EMAIL_LENGTH = 254;
36
- /**
37
- * Maximum local part length per RFC 5321
38
- */
39
- const MAX_LOCAL_PART_LENGTH = 64;
40
- /**
41
- * Validate an email address.
42
- *
43
- * Performs the following checks:
44
- * 1. Not empty or whitespace-only
45
- * 2. Within length limits (254 chars total, 64 chars local part)
46
- * 3. Matches RFC 5322 email format
47
- * 4. Domain has at least one dot (e.g., rejects "user@localhost")
48
- * 5. Optionally blocks disposable email domains
49
- *
50
- * @param email - Email address to validate
51
- * @param options - Validation options
52
- * @returns Validation result with normalized email if valid
53
- *
54
- * @example
55
- * ```typescript
56
- * const result = validateEmail('User@Example.COM');
57
- * if (result.valid) {
58
- * console.log(result.normalized); // 'user@example.com'
59
- * } else {
60
- * console.error(result.message);
61
- * }
62
- * ```
63
- */
64
- export function validateEmail(email, options = {}) {
65
- const { blockDisposable = false, requireTld = true } = options;
66
- // Check for empty/null/undefined
67
- if (!email || typeof email !== 'string') {
68
- return {
69
- valid: false,
70
- error: 'EMPTY',
71
- message: 'Email address is required',
72
- };
73
- }
74
- // Trim whitespace
75
- const trimmed = email.trim();
76
- if (trimmed.length === 0) {
77
- return {
78
- valid: false,
79
- error: 'EMPTY',
80
- message: 'Email address is required',
81
- };
82
- }
83
- // Check total length
84
- if (trimmed.length > MAX_EMAIL_LENGTH) {
85
- return {
86
- valid: false,
87
- error: 'TOO_LONG',
88
- message: `Email address must be ${MAX_EMAIL_LENGTH} characters or less`,
89
- };
90
- }
91
- // Split into local and domain parts
92
- const atIndex = trimmed.lastIndexOf('@');
93
- if (atIndex === -1) {
94
- return {
95
- valid: false,
96
- error: 'INVALID_FORMAT',
97
- message: 'Email address must contain @',
98
- };
99
- }
100
- const localPart = trimmed.substring(0, atIndex);
101
- const domain = trimmed.substring(atIndex + 1);
102
- // Check local part length
103
- if (localPart.length === 0) {
104
- return {
105
- valid: false,
106
- error: 'INVALID_LOCAL_PART',
107
- message: 'Email local part (before @) is required',
108
- };
109
- }
110
- if (localPart.length > MAX_LOCAL_PART_LENGTH) {
111
- return {
112
- valid: false,
113
- error: 'INVALID_LOCAL_PART',
114
- message: `Email local part must be ${MAX_LOCAL_PART_LENGTH} characters or less`,
115
- };
116
- }
117
- // Check domain
118
- if (domain.length === 0) {
119
- return {
120
- valid: false,
121
- error: 'INVALID_DOMAIN',
122
- message: 'Email domain (after @) is required',
123
- };
124
- }
125
- // Require TLD (at least one dot in domain)
126
- if (requireTld && !domain.includes('.')) {
127
- return {
128
- valid: false,
129
- error: 'DOMAIN_TOO_SHORT',
130
- message: 'Email domain must include a valid TLD (e.g., .com, .org)',
131
- };
132
- }
133
- // Check domain TLD length (must be at least 2 chars)
134
- if (requireTld) {
135
- const lastDot = domain.lastIndexOf('.');
136
- const tld = domain.substring(lastDot + 1);
137
- if (tld.length < 2) {
138
- return {
139
- valid: false,
140
- error: 'DOMAIN_TOO_SHORT',
141
- message: 'Email domain TLD must be at least 2 characters',
142
- };
143
- }
144
- }
145
- // Validate format with regex
146
- if (!EMAIL_REGEX.test(trimmed)) {
147
- return {
148
- valid: false,
149
- error: 'INVALID_FORMAT',
150
- message: 'Email address format is invalid',
151
- };
152
- }
153
- // Normalize to lowercase
154
- const normalized = trimmed.toLowerCase();
155
- const normalizedDomain = domain.toLowerCase();
156
- // Check for disposable domains
157
- if (blockDisposable && DISPOSABLE_EMAIL_DOMAINS.has(normalizedDomain)) {
158
- return {
159
- valid: false,
160
- error: 'DISPOSABLE_DOMAIN',
161
- message: 'Disposable email addresses are not allowed',
162
- };
163
- }
164
- return {
165
- valid: true,
166
- normalized,
167
- };
168
- }
169
- /**
170
- * Quick check if an email is valid (without detailed error info).
171
- *
172
- * @param email - Email to check
173
- * @returns true if valid, false otherwise
174
- */
175
- export function isValidEmail(email) {
176
- return validateEmail(email).valid;
177
- }
178
- /**
179
- * Normalize an email address (lowercase, trimmed).
180
- * Returns null if email is invalid.
181
- *
182
- * @param email - Email to normalize
183
- * @returns Normalized email or null if invalid
184
- */
185
- export function normalizeEmail(email) {
186
- const result = validateEmail(email);
187
- return result.valid ? result.normalized : null;
188
- }
189
- /**
190
- * Common passwords to reject (minimal list - use larger database in production)
191
- */
192
- const COMMON_PASSWORDS = new Set([
193
- 'password',
194
- 'password1',
195
- 'password123',
196
- '123456',
197
- '12345678',
198
- '123456789',
199
- '1234567890',
200
- 'qwerty',
201
- 'qwerty123',
202
- 'letmein',
203
- 'welcome',
204
- 'admin',
205
- 'login',
206
- 'abc123',
207
- 'monkey',
208
- 'master',
209
- 'dragon',
210
- 'sunshine',
211
- 'princess',
212
- 'football',
213
- 'baseball',
214
- 'iloveyou',
215
- 'trustno1',
216
- 'superman',
217
- 'batman',
218
- ]);
219
- /**
220
- * Minimum password length
221
- */
222
- const MIN_PASSWORD_LENGTH = 8;
223
- /**
224
- * Maximum password length (bcrypt has a 72-byte limit)
225
- */
226
- const MAX_PASSWORD_LENGTH = 72;
227
- /**
228
- * Validate a password against security requirements.
229
- *
230
- * Requirements:
231
- * - 8-72 characters
232
- * - At least one lowercase letter
233
- * - At least one uppercase letter
234
- * - At least one digit
235
- * - At least one special character
236
- * - Not a common password
237
- *
238
- * @param password - Password to validate
239
- * @param options - Validation options
240
- * @returns Validation result with strength score
241
- */
242
- export function validatePassword(password, options = {}) {
243
- const { minLength = MIN_PASSWORD_LENGTH, requireUppercase = true, requireLowercase = true, requireDigit = true, requireSpecial = true, checkCommon = true, } = options;
244
- const errors = [];
245
- let strength = 0;
246
- // Handle null/undefined
247
- if (!password || typeof password !== 'string') {
248
- return {
249
- valid: false,
250
- errors: ['TOO_SHORT'],
251
- strength: 0,
252
- strengthLabel: 'very_weak',
253
- };
254
- }
255
- // Check length
256
- if (password.length < minLength) {
257
- errors.push('TOO_SHORT');
258
- }
259
- else {
260
- strength++;
261
- }
262
- if (password.length > MAX_PASSWORD_LENGTH) {
263
- errors.push('TOO_LONG');
264
- }
265
- // Check for lowercase
266
- if (requireLowercase && !/[a-z]/.test(password)) {
267
- errors.push('NO_LOWERCASE');
268
- }
269
- else if (/[a-z]/.test(password)) {
270
- strength++;
271
- }
272
- // Check for uppercase
273
- if (requireUppercase && !/[A-Z]/.test(password)) {
274
- errors.push('NO_UPPERCASE');
275
- }
276
- else if (/[A-Z]/.test(password)) {
277
- strength++;
278
- }
279
- // Check for digit
280
- if (requireDigit && !/\d/.test(password)) {
281
- errors.push('NO_DIGIT');
282
- }
283
- else if (/\d/.test(password)) {
284
- strength++;
285
- }
286
- // Check for special character
287
- if (requireSpecial && !/[!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?`~]/.test(password)) {
288
- errors.push('NO_SPECIAL');
289
- }
290
- // Check for common passwords
291
- if (checkCommon && COMMON_PASSWORDS.has(password.toLowerCase())) {
292
- errors.push('COMMON_PASSWORD');
293
- }
294
- // Cap strength at 4
295
- const cappedStrength = Math.min(strength, 4);
296
- const strengthLabels = {
297
- 0: 'very_weak',
298
- 1: 'weak',
299
- 2: 'fair',
300
- 3: 'strong',
301
- 4: 'very_strong',
302
- };
303
- return {
304
- valid: errors.length === 0,
305
- errors,
306
- strength: cappedStrength,
307
- strengthLabel: strengthLabels[cappedStrength],
308
- };
309
- }
310
- /**
311
- * Quick check if a password meets minimum requirements.
312
- *
313
- * @param password - Password to check
314
- * @returns true if valid, false otherwise
315
- */
316
- export function isValidPassword(password) {
317
- return validatePassword(password).valid;
318
- }
@@ -1,124 +0,0 @@
1
- /**
2
- * Safe Evaluation Utilities
3
- *
4
- * This module centralizes all intentional uses of the Function constructor
5
- * to provide a single, well-documented location for dynamic code execution.
6
- *
7
- * SECURITY NOTICE:
8
- * These utilities use the Function constructor which can execute arbitrary code.
9
- * They should ONLY be used in controlled scenarios where:
10
- * 1. Input is strictly validated/sanitized
11
- * 2. The execution context is understood
12
- * 3. There is no viable alternative (e.g., for runtime environment detection)
13
- *
14
- * Each function in this module documents its specific use case and security
15
- * considerations.
16
- */
17
- /**
18
- * Pattern matching only allowed mathematical expression characters.
19
- * Includes: digits, decimal points, basic operators, parentheses, whitespace,
20
- * and common math function names.
21
- */
22
- const SAFE_MATH_EXPRESSION_PATTERN = /^[\d\s+\-*/().%^eE,]+$|^(Math\.(abs|ceil|floor|round|sqrt|pow|min|max|random|sin|cos|tan|log|exp)|\d)+[\d\s+\-*/().%^eE,()]*$/;
23
- /**
24
- * Validate that a mathematical expression contains only safe characters.
25
- *
26
- * @param expression - The expression string to validate
27
- * @returns true if the expression is safe, false otherwise
28
- */
29
- export function isValidMathExpression(expression) {
30
- // Empty or whitespace-only strings are invalid
31
- if (!expression || !expression.trim()) {
32
- return false;
33
- }
34
- // Check for dangerous patterns first
35
- const dangerousPatterns = [
36
- /\beval\b/i,
37
- /\bFunction\b/i,
38
- /\bimport\b/i,
39
- /\brequire\b/i,
40
- /\bglobalThis\b/i,
41
- /\bwindow\b/i,
42
- /\bprocess\b/i,
43
- /\bconstructor\b/i,
44
- /\bprototype\b/i,
45
- /\b__proto__\b/i,
46
- /\[\s*['"]/, // Property access with string literals
47
- /['"`]/, // String literals
48
- ];
49
- for (const pattern of dangerousPatterns) {
50
- if (pattern.test(expression)) {
51
- return false;
52
- }
53
- }
54
- // Check that expression only contains allowed characters
55
- return SAFE_MATH_EXPRESSION_PATTERN.test(expression.trim());
56
- }
57
- /**
58
- * Safely evaluate a mathematical expression.
59
- *
60
- * SECURITY: This function uses the Function constructor for dynamic evaluation.
61
- * It MUST only be called with validated input from isValidMathExpression().
62
- *
63
- * Use case: Calculator functionality requiring dynamic expression evaluation
64
- * Alternative considered: Using a proper expression parser library (recommended
65
- * for production with complex expressions)
66
- *
67
- * @param expression - A pre-validated mathematical expression
68
- * @returns The numeric result of the expression
69
- * @throws Error if evaluation fails or result is not a valid number
70
- *
71
- * @example
72
- * ```typescript
73
- * if (isValidMathExpression(expr)) {
74
- * const result = evaluateMathExpression(expr);
75
- * }
76
- * ```
77
- */
78
- export function evaluateMathExpression(expression) {
79
- // Double-check validation (defense in depth)
80
- if (!isValidMathExpression(expression)) {
81
- throw new Error('Invalid mathematical expression');
82
- }
83
- try {
84
- // eslint-disable-next-line no-new-func
85
- const func = new Function(`"use strict"; return (${expression})`);
86
- const result = func();
87
- if (typeof result !== 'number' || !isFinite(result)) {
88
- throw new Error('Expression did not evaluate to a finite number');
89
- }
90
- return result;
91
- }
92
- catch (error) {
93
- const message = error instanceof Error ? error.message : String(error);
94
- throw new Error(`Expression evaluation failed: ${message}`);
95
- }
96
- }
97
- /**
98
- * Check if the current module is the main entry point (ESM-compatible).
99
- *
100
- * SECURITY: This function uses the Function constructor to access import.meta.url
101
- * in a way that's compatible with both ESM and CommonJS environments without
102
- * causing parse-time errors.
103
- *
104
- * Use case: Runtime environment detection for CLI entry points
105
- * Why Function constructor: import.meta syntax causes parse errors in CommonJS/Jest
106
- * Alternative: Separate entry point files (increases maintenance burden)
107
- *
108
- * @returns true if running as main module, false otherwise
109
- */
110
- export function isMainModule() {
111
- try {
112
- // Use Function constructor to avoid parse-time errors with import.meta in CommonJS/Jest.
113
- // This is a well-known pattern for ESM/CJS interoperability.
114
- // The constructed function only accesses import.meta.url which is safe.
115
- // eslint-disable-next-line no-new-func
116
- const getImportMetaUrl = new Function('return import.meta.url');
117
- const metaUrl = getImportMetaUrl();
118
- return metaUrl === `file://${process.argv[1]}`;
119
- }
120
- catch {
121
- // In CommonJS or environments where import.meta is not available
122
- return false;
123
- }
124
- }