diff --git a/src/constants.ts b/src/constants.ts index 73e4274..cf13341 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -7,5 +7,3 @@ export const DEFAULT_SETTINGS = { maxMessageHistory: 50, lastIndexTime: 0, }; - -// Model validation regex - lowercase letters, numbers, dashes, underscores only diff --git a/src/utils.js b/src/utils.js index 80d3e2d..ea8e2af 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1,176 +1,165 @@ -"use strict"; +'use strict'; // src/utils.ts -Object.defineProperty(exports, "__esModule", { value: true }); +Object.defineProperty(exports, '__esModule', { value: true }); exports.Logger = void 0; exports.validateOllamaUrl = validateOllamaUrl; exports.validateModelName = validateModelName; exports.validatePluginSettings = validatePluginSettings; exports.safeParseJson = safeParseJson; exports.sanitizeFilePath = sanitizeFilePath; -exports.isValidHttpUrl = isValidHttpUrl; + // ==================== Logger ==================== var LogLevel; (function (LogLevel) { - LogLevel[LogLevel["DEBUG"] = 0] = "DEBUG"; - LogLevel[LogLevel["INFO"] = 1] = "INFO"; - LogLevel[LogLevel["WARN"] = 2] = "WARN"; - LogLevel[LogLevel["ERROR"] = 3] = "ERROR"; + LogLevel[(LogLevel['DEBUG'] = 0)] = 'DEBUG'; + LogLevel[(LogLevel['INFO'] = 1)] = 'INFO'; + LogLevel[(LogLevel['WARN'] = 2)] = 'WARN'; + LogLevel[(LogLevel['ERROR'] = 3)] = 'ERROR'; })(LogLevel || (LogLevel = {})); const SEVERITY_ORDER = { - debug: LogLevel.DEBUG, - info: LogLevel.INFO, - warn: LogLevel.WARN, - error: LogLevel.ERROR, + debug: LogLevel.DEBUG, + info: LogLevel.INFO, + warn: LogLevel.WARN, + error: LogLevel.ERROR, }; class Logger { - static setLevel(level) { - if (typeof level === 'string') { - const lowerLevel = level.toLowerCase(); - Logger.minLevel = SEVERITY_ORDER[lowerLevel] ?? LogLevel.DEBUG; - } - else { - Logger.minLevel = level; - } + static setLevel(level) { + if (typeof level === 'string') { + const lowerLevel = level.toLowerCase(); + Logger.minLevel = SEVERITY_ORDER[lowerLevel] ?? LogLevel.DEBUG; + } else { + Logger.minLevel = level; } - static debug(message, category = 'general') { - if (LogLevel.DEBUG >= Logger.minLevel) { - console.debug(`[${category}] DEBUG: ${message}`); - } + } + static debug(message, category = 'general') { + if (LogLevel.DEBUG >= Logger.minLevel) { + console.debug(`[${category}] DEBUG: ${message}`); } - static info(message, category = 'general') { - if (LogLevel.INFO >= Logger.minLevel) { - console.info(`[${category}] INFO: ${message}`); - } + } + static info(message, category = 'general') { + if (LogLevel.INFO >= Logger.minLevel) { + console.info(`[${category}] INFO: ${message}`); } - static warn(message, category = 'general') { - if (LogLevel.WARN >= Logger.minLevel) { - console.warn(`[${category}] WARN: ${message}`); - } + } + static warn(message, category = 'general') { + if (LogLevel.WARN >= Logger.minLevel) { + console.warn(`[${category}] WARN: ${message}`); } - static error(message, category = 'general') { - if (LogLevel.ERROR >= Logger.minLevel) { - console.error(`[${category}] ERROR: ${message}`); - } + } + static error(message, category = 'general') { + if (LogLevel.ERROR >= Logger.minLevel) { + console.error(`[${category}] ERROR: ${message}`); } + } } exports.Logger = Logger; Logger.minLevel = LogLevel.DEBUG; // ==================== URL & Model Validation ==================== function validateOllamaUrl(url) { - if (typeof url !== 'string' || !url.trim()) { - return { valid: false, error: 'URL cannot be empty' }; - } - const trimmedUrl = url.trim(); - if (trimmedUrl.endsWith('/')) { - return { valid: false, error: 'URL should not end with a slash' }; - } - try { - const parsed = new URL(trimmedUrl); - if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') { - return { valid: false, error: 'Must be a valid HTTP or HTTPS URL' }; - } - return { valid: true }; - } - catch { - return { valid: false, error: 'Must be a valid HTTP or HTTPS URL' }; - } -} -function validateModelName(model) { - if (typeof model !== 'string') { - return { valid: false, error: 'Model name must be a string' }; - } - const trimmedModel = model.trim(); - // Explicit check for empty string after trimming - if (!trimmedModel || trimmedModel.length === 0) { - return { valid: false, error: 'Model name cannot be empty' }; - } - if (trimmedModel.length < 2) { - return { valid: false, error: 'Model name must be at least 2 characters long' }; - } - if (trimmedModel.length > 100) { - return { valid: false, error: 'Model name must be less than 100 characters long' }; - } - if (!/^[a-zA-Z0-9._:-]+$/.test(trimmedModel)) { - return { - valid: false, - error: 'Model name can only contain letters, numbers, dots, dashes, underscores, and colons', - }; + if (typeof url !== 'string' || !url.trim()) { + return { valid: false, error: 'URL cannot be empty' }; + } + const trimmedUrl = url.trim(); + if (trimmedUrl.endsWith('/')) { + return { valid: false, error: 'URL should not end with a slash' }; + } + try { + const parsed = new URL(trimmedUrl); + if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') { + return { valid: false, error: 'Must be a valid HTTP or HTTPS URL' }; } return { valid: true }; + } catch { + return { valid: false, error: 'Must be a valid HTTP or HTTPS URL' }; + } +} +function validateModelName(model) { + if (typeof model !== 'string') { + return { valid: false, error: 'Model name must be a string' }; + } + const trimmedModel = model.trim(); + // Explicit check for empty string after trimming + if (!trimmedModel || trimmedModel.length === 0) { + return { valid: false, error: 'Model name cannot be empty' }; + } + if (trimmedModel.length < 2) { + return { valid: false, error: 'Model name must be at least 2 characters long' }; + } + if (trimmedModel.length > 100) { + return { valid: false, error: 'Model name must be less than 100 characters long' }; + } + if (!/^[a-zA-Z0-9._:-]+$/.test(trimmedModel)) { + return { + valid: false, + error: 'Model name can only contain letters, numbers, dots, dashes, underscores, and colons', + }; + } + return { valid: true }; } function validatePluginSettings(settings) { - const errors = []; - const urlValidation = validateOllamaUrl(settings.ollamaUrl); - if (!urlValidation.valid) { - errors.push(`Invalid Ollama URL: ${urlValidation.error}`); - } - const modelValidation = validateModelName(settings.model); - if (!modelValidation.valid) { - errors.push(`Invalid Model Name: ${modelValidation.error}`); - } - return errors; + const errors = []; + const urlValidation = validateOllamaUrl(settings.ollamaUrl); + if (!urlValidation.valid) { + errors.push(`Invalid Ollama URL: ${urlValidation.error}`); + } + const modelValidation = validateModelName(settings.model); + if (!modelValidation.valid) { + errors.push(`Invalid Model Name: ${modelValidation.error}`); + } + return errors; } // ==================== Safe JSON Parsing ==================== const MAX_JSON_SIZE = 1000000; const MAX_JSON_NESTING = 24; function countNestingDepth(value, depth = 0) { - if (depth > MAX_JSON_NESTING) { - return depth; - } - if (Array.isArray(value)) { - return Math.max(...value.map((item) => countNestingDepth(item, depth + 1)), depth); - } - if (value !== null && typeof value === 'object') { - const entries = Object.values(value); - if (entries.length === 0) - return depth; - return Math.max(...entries.map((item) => countNestingDepth(item, depth + 1)), depth); - } + if (depth > MAX_JSON_NESTING) { return depth; + } + if (Array.isArray(value)) { + return Math.max(...value.map((item) => countNestingDepth(item, depth + 1)), depth); + } + if (value !== null && typeof value === 'object') { + const entries = Object.values(value); + if (entries.length === 0) return depth; + return Math.max(...entries.map((item) => countNestingDepth(item, depth + 1)), depth); + } + return depth; } function safeParseJson(jsonString) { - if (typeof jsonString !== 'string') { - throw new Error('Input must be a string'); - } - if (jsonString.length > MAX_JSON_SIZE) { - throw new Error('JSON input too large'); - } - let parsed; - try { - parsed = JSON.parse(jsonString); - } - catch { - throw new Error('Invalid JSON'); - } - // Check for dangerous prototype pollution patterns - const reStringified = JSON.stringify(parsed); - if (reStringified.includes('constructor') || - reStringified.includes('prototype') || - reStringified.includes('__proto__') || - reStringified.includes('function')) { - throw new Error('dangerous code pattern detected'); - } - // Check nesting depth - if (countNestingDepth(parsed) > MAX_JSON_NESTING) { - throw new Error('JSON nesting too deep'); - } - return parsed; + if (typeof jsonString !== 'string') { + throw new Error('Input must be a string'); + } + if (jsonString.length > MAX_JSON_SIZE) { + throw new Error('JSON input too large'); + } + let parsed; + try { + parsed = JSON.parse(jsonString); + } catch { + throw new Error('Invalid JSON'); + } + // Check for dangerous prototype pollution patterns + const reStringified = JSON.stringify(parsed); + if ( + reStringified.includes('constructor') || + reStringified.includes('prototype') || + reStringified.includes('__proto__') || + reStringified.includes('function') + ) { + throw new Error('dangerous code pattern detected'); + } + // Check nesting depth + if (countNestingDepth(parsed) > MAX_JSON_NESTING) { + throw new Error('JSON nesting too deep'); + } + return parsed; } // ==================== Path & File Utilities ==================== function sanitizeFilePath(path) { - if (path.includes('..')) { - throw new Error('Invalid path - cannot contain .. segments'); - } - return path; -} -// ==================== HTTP Helpers ==================== -function isValidHttpUrl(url) { - try { - const parsed = new URL(url); - return parsed.protocol === 'http:' || parsed.protocol === 'https:'; - } - catch { - return false; - } + if (path.includes('..')) { + throw new Error('Invalid path - cannot contain .. segments'); + } + return path; } + // ==================== Markdown Utilities ==================== diff --git a/src/utils.ts b/src/utils.ts index b9f9e81..e58965a 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -188,17 +188,4 @@ export function safeParseJson(jsonString: string): unknown { return parsed; } -// ==================== Path & File Utilities ==================== - -// ==================== HTTP Helpers ==================== - -export function isValidHttpUrl(url: string): boolean { - try { - const parsed = new URL(url); - return parsed.protocol === 'http:' || parsed.protocol === 'https:'; - } catch { - return false; - } -} - // ==================== Markdown Utilities ==================== diff --git a/tests/utils.test.ts b/tests/utils.test.ts index c7f3959..24b7456 100755 --- a/tests/utils.test.ts +++ b/tests/utils.test.ts @@ -7,7 +7,6 @@ import { validateModelName, validatePluginSettings, safeParseJson, - isValidHttpUrl, } from '../src/utils'; describe('Validation Functions', () => { @@ -185,23 +184,6 @@ describe('Validation Functions', () => { expect(() => safeParseJson(123 as any)).toThrow('Input must be a string'); }); }); - - describe('isValidHttpUrl', () => { - it('should accept valid HTTP URLs', () => { - expect(isValidHttpUrl('http://localhost:11434')).toBe(true); - expect(isValidHttpUrl('https://example.com')).toBe(true); - }); - - it('should reject non-HTTP URLs', () => { - expect(isValidHttpUrl('ftp://example.com')).toBe(false); - expect(isValidHttpUrl('file:///path')).toBe(false); - }); - - it('should reject invalid URLs', () => { - expect(isValidHttpUrl('not-a-url')).toBe(false); - expect(isValidHttpUrl('')).toBe(false); - }); - }); }); describe('Logger', () => {