mcp-prompt-optimizer-local
Version:
Advanced cross-platform prompt optimization with MCP integration and 120+ optimization rules
330 lines (280 loc) • 11.9 kB
JavaScript
/**
* Simplified License Manager - Secure with Backend Validation
* All users (including free tier) require API keys validated by backend
*/
const https = require('https');
const fs = require('fs').promises;
const path = require('path');
const os = require('os');
class SimplifiedLicenseManager {
constructor() {
this.cacheFile = path.join(os.homedir(), '.mcp-license-cache.json');
this.cacheExpiry = 1 * 60 * 60 * 1000; // 1 hour cache only
this.backendUrl = process.env.OPTIMIZER_BACKEND_URL || 'https://p01--project-optimizer--fvmrdk8m9k9j.code.run';
this.debug = process.env.MCP_DEBUG === 'true';
}
log(message) {
if (this.debug) {
console.log(`[DEBUG] ${message}`);
}
}
async validateLicense() {
const apiKey = process.env.OPTIMIZER_API_KEY;
this.log(`Starting license validation...`);
this.log(`API Key found: ${apiKey ? 'YES' : 'NO'}`);
// SECURITY: API key is REQUIRED - no free tier bypass
if (!apiKey) {
this.log(`No API key provided - validation FAILED`);
return {
valid: false,
error: 'API key required. Sign up for a free key at https://promptoptimizer-blog.vercel.app/local-license'
};
}
this.log(`API Key: ${apiKey.substring(0, 25)}...`);
// Validate format
if (!this.isValidKeyFormat(apiKey)) {
this.log(`Key format validation failed`);
return {
valid: false,
error: 'Invalid API key format. Must start with "sk-local-" and contain "-basic-" or "-pro-"'
};
}
this.log(`Key format validation passed`);
// Try cache first (short-lived for performance)
const cached = await this.getCachedValidation(apiKey);
if (cached && cached.valid) {
this.log(`Using cached validation - VALID`);
return cached;
}
this.log(`No valid cache found, validating with backend...`);
// SECURITY: Must validate with backend server
try {
const backendValidation = await this.validateWithBackend(apiKey);
if (backendValidation && backendValidation.valid) {
this.log(`Backend validation successful`);
// Cache successful validation
await this.cacheValidation(apiKey, backendValidation);
return backendValidation;
}
return {
valid: false,
error: backendValidation?.error || 'Invalid API key'
};
} catch (error) {
this.log(`Backend validation failed: ${error.message}`);
return {
valid: false,
error: `Validation failed: ${error.message}`
};
}
}
// SECURITY: Backend validation method
async validateWithBackend(apiKey) {
return new Promise((resolve, reject) => {
const url = `${this.backendUrl}/api/v1/api-keys/validate`;
const options = {
method: 'POST',
headers: {
'x-api-key': apiKey,
'Content-Type': 'application/json',
'Accept': 'application/json'
},
timeout: 10000
};
const req = https.request(url, options, (res) => {
let data = '';
res.on('data', (chunk) => {
data += chunk;
});
res.on('end', () => {
try {
if (res.statusCode === 200) {
const response = JSON.parse(data);
if (response.valid && response.context) {
// Map backend response to our format
resolve({
valid: true,
type: response.context.type,
tier: response.context.tier,
quota: {
limit: response.context.quota_limit,
used: response.context.quota_used,
remaining: response.context.quota_limit === null ? 'unlimited' :
response.context.quota_limit - response.context.quota_used,
unlimited: response.context.quota_limit === null
},
user_id: response.context.user_id,
mcp_access_level: response.context.mcp_access_level
});
} else {
resolve({ valid: false, error: 'Invalid API key' });
}
} else if (res.statusCode === 401) {
resolve({ valid: false, error: 'Invalid API key' });
} else {
const error = JSON.parse(data);
resolve({ valid: false, error: error.detail || 'Validation failed' });
}
} catch (parseError) {
reject(new Error(`Invalid response: ${parseError.message}`));
}
});
});
req.on('error', (error) => {
reject(new Error(`Network error: ${error.message}`));
});
req.on('timeout', () => {
req.destroy();
reject(new Error('Request timeout'));
});
req.end();
});
}
// SECURITY: Get quota status from backend
async getQuotaStatus() {
const apiKey = process.env.OPTIMIZER_API_KEY;
if (!apiKey) {
return { error: 'API key required' };
}
try {
return new Promise((resolve, reject) => {
const url = `${this.backendUrl}/api/v1/api-keys/quota-status`;
const options = {
method: 'GET',
headers: {
'x-api-key': apiKey,
'Accept': 'application/json'
},
timeout: 10000
};
const req = https.request(url, options, (res) => {
let data = '';
res.on('data', (chunk) => {
data += chunk;
});
res.on('end', () => {
try {
if (res.statusCode === 200) {
const response = JSON.parse(data);
resolve({
tier: response.tier,
unlimited: response.quota_limit === null,
used: response.quota_used,
remaining: response.quota_remaining,
limit: response.quota_limit,
usage_percentage: response.usage_percentage
});
} else {
resolve({ error: 'Failed to get quota status' });
}
} catch (parseError) {
reject(new Error(`Invalid response: ${parseError.message}`));
}
});
});
req.on('error', (error) => {
reject(new Error(`Network error: ${error.message}`));
});
req.on('timeout', () => {
req.destroy();
reject(new Error('Request timeout'));
});
req.end();
});
} catch (error) {
this.log(`Error getting quota status: ${error.message}`);
return { error: error.message };
}
}
// Use the exact validation logic from the original working version
isValidKeyFormat(licenseKey) {
if (!licenseKey || typeof licenseKey !== 'string') {
this.log(`Key validation failed: not a string`);
return false;
}
// Must start with sk-local-
if (!licenseKey.startsWith('sk-local-')) {
this.log(`Key validation failed: doesn't start with sk-local-`);
return false;
}
// Must have basic or pro tier
if (!licenseKey.includes('-basic-') && !licenseKey.includes('-pro-')) {
this.log(`Key validation failed: doesn't contain -basic- or -pro-`);
return false;
}
// Minimum length check
if (licenseKey.length < 25) {
this.log(`Key validation failed: too short (${licenseKey.length} chars)`);
return false;
}
this.log(`Key format validation: PASSED`);
return true;
}
async getCachedValidation(apiKey, allowExpired = false) {
try {
const cacheData = await fs.readFile(this.cacheFile, 'utf8');
const cache = JSON.parse(cacheData);
this.log(`Cache file contents: ${JSON.stringify(cache, null, 2)}`);
if (cache.apiKey !== apiKey) {
this.log(`Cache key mismatch`);
return null;
}
const age = Date.now() - new Date(cache.timestamp).getTime();
if (!allowExpired && age > this.cacheExpiry) {
this.log(`Cache expired (${Math.round(age / 1000 / 60)} minutes old)`);
return null;
}
this.log(`Cache found - Valid: ${cache.validation?.valid}`);
return cache.validation;
} catch (error) {
this.log(`No cache available: ${error.message}`);
return null;
}
}
async cacheValidation(apiKey, validation) {
try {
const cacheData = {
apiKey,
validation,
timestamp: new Date().toISOString()
};
await fs.writeFile(this.cacheFile, JSON.stringify(cacheData, null, 2));
this.log(`Validation cached successfully`);
} catch (error) {
this.log(`Cache write failed: ${error.message}`);
}
}
async clearCache() {
try {
await fs.unlink(this.cacheFile);
this.log(`Cache file deleted: ${this.cacheFile}`);
} catch (error) {
this.log(`Cache clear failed (file may not exist): ${error.message}`);
}
}
// NEW: Get current quota status for tools
async getQuotaStatus() {
const license = await this.validateLicense();
if (!license.valid) {
return { error: 'Invalid license' };
}
if (!license.quota || license.quota.unlimited) {
return {
tier: license.tier || license.type,
unlimited: true,
message: 'Unlimited optimizations available'
};
}
const usage = await this.loadDailyUsage();
const remaining = Math.max(0, license.quota.daily_limit - usage.count);
return {
tier: license.tier || license.type,
unlimited: false,
used: usage.count,
remaining,
limit: license.quota.daily_limit,
resetsAt: new Date(new Date().setHours(24, 0, 0, 0)).toISOString()
};
}
}
module.exports = SimplifiedLicenseManager;