UNPKG

@hhoangphuoc/escape-room-cli

Version:

A CLI for playing AI-generated escape room games. Install globally with: npm install -g @hhoangphuoc/escape-room-cli

289 lines (288 loc) 9.44 kB
// Comprehensive validation utilities for CLI authentication system import fs from 'fs'; import path from 'path'; import os from 'os'; import { getApiUrl } from './apiConfig.js'; const USER_CONFIG_FILE = path.join(os.homedir(), '.escape-room-config.json'); /** * Validates OpenAI API key format and functionality */ export const validateOpenAIApiKey = async (apiKey) => { // Format validation - OpenAI keys start with "sk-" and are exactly 51 characters if (!apiKey || typeof apiKey !== 'string') { return { isValid: false, error: 'API key is required', details: 'Please provide your OpenAI API key' }; } const trimmedKey = apiKey.trim(); // Check format: newer keys start with "sk-proj-" or older ones with "sk-" if (!trimmedKey.startsWith('sk-proj-') && !trimmedKey.startsWith('sk-')) { return { isValid: false, error: 'Invalid OpenAI API key format', details: 'OpenAI API keys must start with "sk-proj-" or "sk-"' }; } // Basic length check - should be at least 20 characters if (trimmedKey.length < 20) { return { isValid: false, error: 'Invalid OpenAI API key length', details: 'OpenAI API keys must be at least 20 characters long' }; } // Backend validation - make a harmless API call to verify the key works try { const apiUrl = getApiUrl(); const response = await fetch(`${apiUrl}/api/users/validate/openai-key`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ apiKey: trimmedKey }) }); const data = await response.json(); if (response.ok && data.valid) { return { isValid: true, provider: 'openai' }; } else { return { isValid: false, error: 'OpenAI API key verification failed', details: data.error || 'The API key could not be verified with OpenAI servers' }; } } catch (error) { // If backend validation fails, we'll accept the key if format is correct // This allows the system to work even if the backend is temporarily unavailable console.warn('Backend API key validation failed, accepting based on format:', error); return { isValid: true, provider: 'openai', details: 'API key accepted based on format (backend verification unavailable)' }; } }; /** * Validates Anthropic API key format */ export const validateAnthropicApiKey = (apiKey) => { if (!apiKey || typeof apiKey !== 'string') { return { isValid: false, error: 'API key is required', details: 'Please provide your Anthropic API key' }; } const trimmedKey = apiKey.trim(); // Anthropic keys typically start with "anthropic" or "sk-ant-" if (!trimmedKey.startsWith('sk-ant-') && !trimmedKey.startsWith('anthropic')) { return { isValid: false, error: 'Invalid Anthropic API key format', details: 'Anthropic API keys typically start with "sk-ant-" or "anthropic"' }; } if (trimmedKey.length < 20) { return { isValid: false, error: 'Invalid Anthropic API key length', details: 'Anthropic API keys must be at least 20 characters long' }; } return { isValid: true, provider: 'anthropic' }; }; /** * Comprehensive email validation */ export const validateEmail = async (email) => { if (!email || typeof email !== 'string') { return { isValid: false, error: 'Email is required', details: 'Please provide a valid email address' }; } const trimmedEmail = email.trim().toLowerCase(); // Basic email format validation const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; if (!emailRegex.test(trimmedEmail)) { return { isValid: false, error: 'Invalid email format', details: 'Please enter a valid email address (e.g., user@example.com)' }; } // Check for common invalid patterns if (trimmedEmail.includes('..') || trimmedEmail.startsWith('.') || trimmedEmail.endsWith('.')) { return { isValid: false, error: 'Invalid email format', details: 'Email cannot contain consecutive dots or start/end with dots' }; } // Check against local config files for duplicates const isDuplicate = checkEmailInLocalConfig(trimmedEmail); if (isDuplicate) { return { isValid: false, error: 'Email already registered locally', details: 'This email is already associated with a local configuration', isDuplicate: true }; } // Check with Firebase if email already exists try { const existsInFirebase = await checkEmailInFirebase(trimmedEmail); if (existsInFirebase) { return { isValid: false, error: 'Email already registered. Please use a different email address or try /login', details: 'This email is already registered in the system', existsInFirebase: true }; } } catch (error) { // If Firebase check fails, we'll allow the email and let the backend handle it console.warn('Firebase email check failed:', error); } return { isValid: true }; }; /** * Check if email exists in local configuration */ const checkEmailInLocalConfig = (email) => { try { if (!fs.existsSync(USER_CONFIG_FILE)) { return false; } const config = JSON.parse(fs.readFileSync(USER_CONFIG_FILE, 'utf-8')); return config.email && config.email.toLowerCase() === email.toLowerCase(); } catch (error) { console.warn('Error reading local config for email check:', error); return false; } }; /** * Check if email exists in Firebase */ const checkEmailInFirebase = async (email) => { try { const apiUrl = getApiUrl(); const response = await fetch(`${apiUrl}/api/users/validate/email-exists`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email }) }); if (response.ok) { const data = await response.json(); return data.exists === true; } else { // If the endpoint doesn't exist or fails, assume email is available return false; } } catch (error) { // Network error - assume email is available throw error; } }; /** * Validate name/username */ export const validateName = (name) => { if (!name || typeof name !== 'string') { return { isValid: false, error: 'Name is required', details: 'Please provide your name' }; } const trimmedName = name.trim(); if (trimmedName.length < 2) { return { isValid: false, error: 'Name too short', details: 'Name must be at least 2 characters long' }; } if (trimmedName.length > 50) { return { isValid: false, error: 'Name too long', details: 'Name must be less than 50 characters long' }; } // Check for valid characters (letters, numbers, spaces, hyphens, apostrophes) const nameRegex = /^[a-zA-Z0-9\s\-']+$/; if (!nameRegex.test(trimmedName)) { return { isValid: false, error: 'Invalid characters in name', details: 'Name can only contain letters, numbers, spaces, hyphens, and apostrophes' }; } return { isValid: true }; }; /** * Comprehensive validation for all registration fields */ export const validateRegistrationData = async (data) => { const errors = {}; const details = {}; let isValid = true; // Validate name const nameValidation = validateName(data.name); if (!nameValidation.isValid) { errors.name = nameValidation.error; details.name = nameValidation.details; isValid = false; } // Validate email if provided if (data.email) { const emailValidation = await validateEmail(data.email); if (!emailValidation.isValid) { errors.email = emailValidation.error; details.email = emailValidation.details; isValid = false; } } // Validate API key if provided if (data.apiKey) { let apiKeyValidation; if (data.provider === 'openai' || data.apiKey.startsWith('sk-')) { apiKeyValidation = await validateOpenAIApiKey(data.apiKey); } else { apiKeyValidation = validateAnthropicApiKey(data.apiKey); } if (!apiKeyValidation.isValid) { errors.apiKey = apiKeyValidation.error; details.apiKey = apiKeyValidation.details; isValid = false; } } return { isValid, errors, details }; };