@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
JavaScript
// 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
};
};