Files
obsidian_ollama/src/utils.ts
T
fegger 75fa07f148 ```
Remove unused isValidHttpUrl function and related tests

Remove commented-out model validation regex from constants
```
2026-05-07 00:02:32 +02:00

192 lines
5.2 KiB
TypeScript

export enum LogLevel {
DEBUG = 0,
INFO = 1,
WARN = 2,
ERROR = 3,
}
const SEVERITY_ORDER: Record<string, number> = {
debug: LogLevel.DEBUG,
info: LogLevel.INFO,
warn: LogLevel.WARN,
error: LogLevel.ERROR,
};
export class Logger {
private static minLevel: LogLevel = LogLevel.DEBUG;
static setLevel(level: string | LogLevel): void {
if (typeof level === 'string') {
const lowerLevel = level.toLowerCase();
Logger.minLevel = SEVERITY_ORDER[lowerLevel] ?? LogLevel.DEBUG;
} else {
Logger.minLevel = level;
}
}
static debug(message: string, category: string = 'general'): void {
if (LogLevel.DEBUG >= Logger.minLevel) {
console.debug(`[${category}] DEBUG: ${message}`);
}
}
static info(message: string, category: string = 'general'): void {
if (LogLevel.INFO >= Logger.minLevel) {
console.info(`[${category}] INFO: ${message}`);
}
}
static warn(message: string, category: string = 'general'): void {
if (LogLevel.WARN >= Logger.minLevel) {
console.warn(`[${category}] WARN: ${message}`);
}
}
static error(message: string, category: string = 'general'): void {
if (LogLevel.ERROR >= Logger.minLevel) {
console.error(`[${category}] ERROR: ${message}`);
}
}
}
// ==================== URL & Model Validation ====================
export function validateOllamaUrl(url: string): { valid: boolean; error?: string } {
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' };
}
}
export function validateModelName(model: string): { valid: boolean; error?: string } {
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 };
}
export function validatePluginSettings(settings: { ollamaUrl: string; model: string }): string[] {
const errors: string[] = [];
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 = 1_000_000;
const MAX_JSON_NESTING = 24;
function countNestingDepth(value: unknown, depth: number = 0): number {
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 as Record<string, unknown>);
if (entries.length === 0) return depth;
return Math.max(...entries.map((item) => countNestingDepth(item, depth + 1)), depth);
}
return depth;
}
export function safeParseJson(jsonString: string): unknown {
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: unknown;
try {
parsed = JSON.parse(jsonString);
} catch {
throw new Error('Invalid JSON');
}
// Check for dangerous prototype pollution patterns in object keys only
const checkDangerousPatterns = (obj: unknown): boolean => {
if (typeof obj !== 'object' || obj === null) {
return false;
}
const dangerousKeys = ['constructor', 'prototype', '__proto__'];
if (dangerousKeys.some((key) => Object.keys(obj).includes(key))) {
return true;
}
// Recursively check nested objects
for (const key in obj as Record<string, unknown>) {
if (checkDangerousPatterns((obj as Record<string, unknown>)[key])) {
return true;
}
}
return false;
};
if (checkDangerousPatterns(parsed)) {
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;
}
// ==================== Markdown Utilities ====================