mirror of
https://github.com/czlonkowski/n8n-mcp.git
synced 2026-02-06 05:23:08 +00:00
Implemented three high-value fixes identified in code review:
1. NPM Registry Response Validation (npm-version-checker.ts)
- Added NpmRegistryResponse TypeScript interface
- Added JSON parsing validation with try-catch error handling
- Added response structure validation (checking required fields)
- Added semver format validation with regex pattern
- Prevents crashes from malformed npm registry responses
2. TypeScript Type Safety (handlers-n8n-manager.ts)
- Added 5 comprehensive TypeScript interfaces:
* HealthCheckResponseData
* CloudPlatformGuide
* WorkflowValidationResponse
* DiagnosticResponseData
- Replaced 'any' types with proper interfaces in 6 locations
- Imported ExpressionFormatIssue from expression-format-validator
- Improved compile-time type checking and IDE support
3. Cache Hit Rate Calculation (handlers-n8n-manager.ts)
- Improved division-by-zero protection
- Changed condition from 'size > 0' to explicit operation count check
- More robust against edge cases in cache metrics
All changes verified with:
- TypeScript compilation (0 errors)
- Integration tests (195/195 passed)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
209 lines
5.7 KiB
TypeScript
209 lines
5.7 KiB
TypeScript
/**
|
|
* NPM Version Checker Utility
|
|
*
|
|
* Checks if the current n8n-mcp version is outdated by comparing
|
|
* against the latest version published on npm.
|
|
*/
|
|
|
|
import { logger } from './logger';
|
|
|
|
/**
|
|
* NPM Registry Response structure
|
|
* Based on npm registry JSON format for package metadata
|
|
*/
|
|
interface NpmRegistryResponse {
|
|
version: string;
|
|
[key: string]: unknown;
|
|
}
|
|
|
|
export interface VersionCheckResult {
|
|
currentVersion: string;
|
|
latestVersion: string | null;
|
|
isOutdated: boolean;
|
|
updateAvailable: boolean;
|
|
error: string | null;
|
|
checkedAt: Date;
|
|
updateCommand?: string;
|
|
}
|
|
|
|
// Cache for version check to avoid excessive npm requests
|
|
let versionCheckCache: VersionCheckResult | null = null;
|
|
let lastCheckTime: number = 0;
|
|
const CACHE_TTL_MS = 1 * 60 * 60 * 1000; // 1 hour cache
|
|
|
|
/**
|
|
* Check if current version is outdated compared to npm registry
|
|
* Uses caching to avoid excessive npm API calls
|
|
*
|
|
* @param forceRefresh - Force a fresh check, bypassing cache
|
|
* @returns Version check result
|
|
*/
|
|
export async function checkNpmVersion(forceRefresh: boolean = false): Promise<VersionCheckResult> {
|
|
const now = Date.now();
|
|
|
|
// Return cached result if available and not expired
|
|
if (!forceRefresh && versionCheckCache && (now - lastCheckTime) < CACHE_TTL_MS) {
|
|
logger.debug('Returning cached npm version check result');
|
|
return versionCheckCache;
|
|
}
|
|
|
|
// Get current version from package.json
|
|
const packageJson = require('../../package.json');
|
|
const currentVersion = packageJson.version;
|
|
|
|
try {
|
|
// Fetch latest version from npm registry
|
|
const response = await fetch('https://registry.npmjs.org/n8n-mcp/latest', {
|
|
headers: {
|
|
'Accept': 'application/json',
|
|
},
|
|
signal: AbortSignal.timeout(5000) // 5 second timeout
|
|
});
|
|
|
|
if (!response.ok) {
|
|
logger.warn('Failed to fetch npm version info', {
|
|
status: response.status,
|
|
statusText: response.statusText
|
|
});
|
|
|
|
const result: VersionCheckResult = {
|
|
currentVersion,
|
|
latestVersion: null,
|
|
isOutdated: false,
|
|
updateAvailable: false,
|
|
error: `npm registry returned ${response.status}`,
|
|
checkedAt: new Date()
|
|
};
|
|
|
|
versionCheckCache = result;
|
|
lastCheckTime = now;
|
|
return result;
|
|
}
|
|
|
|
// Parse and validate JSON response
|
|
let data: unknown;
|
|
try {
|
|
data = await response.json();
|
|
} catch (error) {
|
|
throw new Error('Failed to parse npm registry response as JSON');
|
|
}
|
|
|
|
// Validate response structure
|
|
if (!data || typeof data !== 'object' || !('version' in data)) {
|
|
throw new Error('Invalid response format from npm registry');
|
|
}
|
|
|
|
const registryData = data as NpmRegistryResponse;
|
|
const latestVersion = registryData.version;
|
|
|
|
// Validate version format (semver: x.y.z or x.y.z-prerelease)
|
|
if (!latestVersion || !/^\d+\.\d+\.\d+/.test(latestVersion)) {
|
|
throw new Error(`Invalid version format from npm registry: ${latestVersion}`);
|
|
}
|
|
|
|
// Compare versions
|
|
const isOutdated = compareVersions(currentVersion, latestVersion) < 0;
|
|
|
|
const result: VersionCheckResult = {
|
|
currentVersion,
|
|
latestVersion,
|
|
isOutdated,
|
|
updateAvailable: isOutdated,
|
|
error: null,
|
|
checkedAt: new Date(),
|
|
updateCommand: isOutdated ? `npm install -g n8n-mcp@${latestVersion}` : undefined
|
|
};
|
|
|
|
// Cache the result
|
|
versionCheckCache = result;
|
|
lastCheckTime = now;
|
|
|
|
logger.debug('npm version check completed', {
|
|
current: currentVersion,
|
|
latest: latestVersion,
|
|
outdated: isOutdated
|
|
});
|
|
|
|
return result;
|
|
|
|
} catch (error) {
|
|
logger.warn('Error checking npm version', {
|
|
error: error instanceof Error ? error.message : String(error)
|
|
});
|
|
|
|
const result: VersionCheckResult = {
|
|
currentVersion,
|
|
latestVersion: null,
|
|
isOutdated: false,
|
|
updateAvailable: false,
|
|
error: error instanceof Error ? error.message : 'Unknown error',
|
|
checkedAt: new Date()
|
|
};
|
|
|
|
// Cache error result to avoid rapid retry
|
|
versionCheckCache = result;
|
|
lastCheckTime = now;
|
|
|
|
return result;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Compare two semantic version strings
|
|
* Returns: -1 if v1 < v2, 0 if v1 === v2, 1 if v1 > v2
|
|
*
|
|
* @param v1 - First version (e.g., "1.2.3")
|
|
* @param v2 - Second version (e.g., "1.3.0")
|
|
* @returns Comparison result
|
|
*/
|
|
export function compareVersions(v1: string, v2: string): number {
|
|
// Remove 'v' prefix if present
|
|
const clean1 = v1.replace(/^v/, '');
|
|
const clean2 = v2.replace(/^v/, '');
|
|
|
|
// Split into parts and convert to numbers
|
|
const parts1 = clean1.split('.').map(n => parseInt(n, 10) || 0);
|
|
const parts2 = clean2.split('.').map(n => parseInt(n, 10) || 0);
|
|
|
|
// Compare each part
|
|
for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) {
|
|
const p1 = parts1[i] || 0;
|
|
const p2 = parts2[i] || 0;
|
|
|
|
if (p1 < p2) return -1;
|
|
if (p1 > p2) return 1;
|
|
}
|
|
|
|
return 0; // Versions are equal
|
|
}
|
|
|
|
/**
|
|
* Clear the version check cache (useful for testing)
|
|
*/
|
|
export function clearVersionCheckCache(): void {
|
|
versionCheckCache = null;
|
|
lastCheckTime = 0;
|
|
}
|
|
|
|
/**
|
|
* Format version check result as a user-friendly message
|
|
*
|
|
* @param result - Version check result
|
|
* @returns Formatted message
|
|
*/
|
|
export function formatVersionMessage(result: VersionCheckResult): string {
|
|
if (result.error) {
|
|
return `Version check failed: ${result.error}. Current version: ${result.currentVersion}`;
|
|
}
|
|
|
|
if (!result.latestVersion) {
|
|
return `Current version: ${result.currentVersion} (latest version unknown)`;
|
|
}
|
|
|
|
if (result.isOutdated) {
|
|
return `⚠️ Update available! Current: ${result.currentVersion} → Latest: ${result.latestVersion}`;
|
|
}
|
|
|
|
return `✓ You're up to date! Current version: ${result.currentVersion}`;
|
|
}
|