@voilajsx/appkit
Version:
Minimal and framework agnostic Node.js toolkit designed for AI agentic backend development
358 lines • 14.6 kB
JavaScript
/**
* Smart defaults and environment validation for file storage with auto-strategy detection
* @module @voilajsx/appkit/storage
* @file src/storage/defaults.ts
*
* @llm-rule WHEN: App startup - need to configure storage system and connection strategy
* @llm-rule AVOID: Calling multiple times - expensive environment parsing, use lazy loading in get()
* @llm-rule NOTE: Called once at startup, cached globally for performance
* @llm-rule NOTE: Auto-detects Local vs S3 vs R2 based on environment variables
*/
/**
* Gets smart defaults using environment variables with auto-strategy detection
* @llm-rule WHEN: App startup to get production-ready storage configuration
* @llm-rule AVOID: Calling repeatedly - expensive validation, cache the result
* @llm-rule NOTE: Auto-detects strategy: S3/R2 env vars → Cloud, nothing → Local
*/
export function getSmartDefaults() {
validateEnvironment();
const nodeEnv = process.env.NODE_ENV || 'development';
const isDevelopment = nodeEnv === 'development';
const isProduction = nodeEnv === 'production';
const isTest = nodeEnv === 'test';
// Auto-detect strategy from environment
const strategy = detectStorageStrategy();
return {
// Strategy selection with smart detection
strategy,
// Local configuration (only used when strategy is 'local')
local: {
dir: process.env.VOILA_STORAGE_DIR || './uploads',
baseUrl: process.env.VOILA_STORAGE_BASE_URL || '/uploads',
maxFileSize: parseInt(process.env.VOILA_STORAGE_MAX_SIZE || '52428800'), // 50MB default
allowedTypes: parseAllowedTypes(),
createDirs: process.env.VOILA_STORAGE_CREATE_DIRS !== 'false',
},
// S3 configuration (only used when strategy is 's3')
s3: {
bucket: process.env.AWS_S3_BUCKET || process.env.S3_BUCKET || '',
region: process.env.AWS_REGION || process.env.S3_REGION || 'us-east-1',
endpoint: process.env.S3_ENDPOINT,
accessKeyId: process.env.AWS_ACCESS_KEY_ID || process.env.S3_ACCESS_KEY_ID || '',
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY || process.env.S3_SECRET_ACCESS_KEY || '',
forcePathStyle: process.env.S3_FORCE_PATH_STYLE === 'true',
signedUrlExpiry: parseInt(process.env.VOILA_STORAGE_SIGNED_EXPIRY || '3600'), // 1 hour
cdnUrl: process.env.VOILA_STORAGE_CDN_URL,
},
// R2 configuration (only used when strategy is 'r2')
r2: {
bucket: process.env.CLOUDFLARE_R2_BUCKET || '',
accountId: process.env.CLOUDFLARE_ACCOUNT_ID || '',
accessKeyId: process.env.CLOUDFLARE_R2_ACCESS_KEY_ID || '',
secretAccessKey: process.env.CLOUDFLARE_R2_SECRET_ACCESS_KEY || '',
cdnUrl: process.env.CLOUDFLARE_R2_CDN_URL,
signedUrlExpiry: parseInt(process.env.VOILA_STORAGE_SIGNED_EXPIRY || '3600'), // 1 hour
},
// Environment information
environment: {
isDevelopment,
isProduction,
isTest,
nodeEnv,
},
};
}
/**
* Auto-detect storage strategy from environment variables
* @llm-rule WHEN: Determining which storage strategy to use automatically
* @llm-rule AVOID: Manual strategy selection - environment detection handles most cases
* @llm-rule NOTE: Priority: R2 → S3 → Local (R2 has zero egress fees)
*/
function detectStorageStrategy() {
// Explicit override wins (for testing/debugging)
const explicit = process.env.VOILA_STORAGE_STRATEGY?.toLowerCase();
if (explicit && ['local', 's3', 'r2'].includes(explicit)) {
return explicit;
}
// Auto-detection logic - prioritize R2 for cost savings
if (process.env.CLOUDFLARE_R2_BUCKET) {
return 'r2'; // Cloudflare R2 - zero egress fees
}
if (process.env.AWS_S3_BUCKET || process.env.S3_BUCKET || process.env.S3_ENDPOINT) {
return 's3'; // S3-compatible services
}
// Default to local for development/single server
if (process.env.NODE_ENV === 'production') {
console.warn('[VoilaJSX AppKit] No cloud storage configured in production. ' +
'Using local filesystem which may not scale. ' +
'Set AWS_S3_BUCKET or CLOUDFLARE_R2_BUCKET for cloud storage.');
}
return 'local'; // Default to local filesystem
}
/**
* Parse allowed file types from environment with safe defaults
* @llm-rule WHEN: Setting up file type restrictions for security
* @llm-rule AVOID: Allowing all file types in production - security risk
*/
function parseAllowedTypes() {
const envTypes = process.env.VOILA_STORAGE_ALLOWED_TYPES;
if (!envTypes) {
// Safe defaults - common web file types
return [
'image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/svg+xml',
'text/plain', 'text/csv', 'application/json',
'application/pdf', 'application/zip',
'video/mp4', 'video/webm', 'audio/mpeg', 'audio/wav'
];
}
if (envTypes === '*') {
if (process.env.NODE_ENV === 'production') {
console.warn('[VoilaJSX AppKit] SECURITY WARNING: All file types allowed in production. ' +
'Set VOILA_STORAGE_ALLOWED_TYPES to specific types for security.');
}
return ['*']; // Allow all types (use with caution)
}
return envTypes.split(',').map(type => type.trim()).filter(Boolean);
}
/**
* Validates environment variables for storage configuration
* @llm-rule WHEN: App startup to ensure proper storage environment configuration
* @llm-rule AVOID: Skipping validation - improper config causes runtime failures
* @llm-rule NOTE: Validates cloud credentials, bucket names, and numeric values
*/
function validateEnvironment() {
// Validate storage strategy if explicitly set
const strategy = process.env.VOILA_STORAGE_STRATEGY;
if (strategy && !['local', 's3', 'r2'].includes(strategy.toLowerCase())) {
throw new Error(`Invalid VOILA_STORAGE_STRATEGY: "${strategy}". Must be "local", "s3", or "r2"`);
}
// Validate numeric values
validateNumericEnv('VOILA_STORAGE_MAX_SIZE', 1048576, 1073741824); // 1MB to 1GB
validateNumericEnv('VOILA_STORAGE_SIGNED_EXPIRY', 60, 604800); // 1 minute to 7 days
// Validate S3 configuration if S3 strategy detected
if (shouldValidateS3()) {
validateS3Config();
}
// Validate R2 configuration if R2 strategy detected
if (shouldValidateR2()) {
validateR2Config();
}
// Validate local configuration if local strategy
if (shouldValidateLocal()) {
validateLocalConfig();
}
// Production-specific validations
const nodeEnv = process.env.NODE_ENV;
if (nodeEnv === 'production') {
validateProductionConfig();
}
// Validate NODE_ENV
if (nodeEnv && !['development', 'production', 'test', 'staging'].includes(nodeEnv)) {
console.warn(`[VoilaJSX AppKit] Unusual NODE_ENV: "${nodeEnv}". ` +
`Expected: development, production, test, or staging`);
}
}
/**
* Check if S3 validation is needed
*/
function shouldValidateS3() {
return !!(process.env.AWS_S3_BUCKET || process.env.S3_BUCKET || process.env.S3_ENDPOINT);
}
/**
* Check if R2 validation is needed
*/
function shouldValidateR2() {
return !!process.env.CLOUDFLARE_R2_BUCKET;
}
/**
* Check if local validation is needed
*/
function shouldValidateLocal() {
const strategy = detectStorageStrategy();
return strategy === 'local';
}
/**
* Validates S3 configuration
*/
function validateS3Config() {
const bucket = process.env.AWS_S3_BUCKET || process.env.S3_BUCKET;
if (!bucket) {
throw new Error('S3 bucket name required. Set AWS_S3_BUCKET or S3_BUCKET environment variable');
}
if (!isValidBucketName(bucket)) {
throw new Error(`Invalid S3 bucket name: "${bucket}". Must be 3-63 characters, lowercase, no dots`);
}
const accessKey = process.env.AWS_ACCESS_KEY_ID || process.env.S3_ACCESS_KEY_ID;
const secretKey = process.env.AWS_SECRET_ACCESS_KEY || process.env.S3_SECRET_ACCESS_KEY;
if (!accessKey || !secretKey) {
throw new Error('S3 credentials required. Set AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY environment variables');
}
const endpoint = process.env.S3_ENDPOINT;
if (endpoint && !isValidUrl(endpoint)) {
throw new Error(`Invalid S3 endpoint: "${endpoint}". Must be a valid URL`);
}
}
/**
* Validates R2 configuration
*/
function validateR2Config() {
const bucket = process.env.CLOUDFLARE_R2_BUCKET;
if (!bucket) {
throw new Error('R2 bucket name required. Set CLOUDFLARE_R2_BUCKET environment variable');
}
if (!isValidBucketName(bucket)) {
throw new Error(`Invalid R2 bucket name: "${bucket}". Must be 3-63 characters, lowercase`);
}
const accountId = process.env.CLOUDFLARE_ACCOUNT_ID;
const accessKey = process.env.CLOUDFLARE_R2_ACCESS_KEY_ID;
const secretKey = process.env.CLOUDFLARE_R2_SECRET_ACCESS_KEY;
if (!accountId || !accessKey || !secretKey) {
throw new Error('R2 credentials required. Set CLOUDFLARE_ACCOUNT_ID, CLOUDFLARE_R2_ACCESS_KEY_ID, and CLOUDFLARE_R2_SECRET_ACCESS_KEY environment variables');
}
}
/**
* Validates local configuration
*/
function validateLocalConfig() {
const dir = process.env.VOILA_STORAGE_DIR;
if (dir && (dir.includes('..') || dir.startsWith('/') && process.env.NODE_ENV === 'production')) {
console.warn(`[VoilaJSX AppKit] Potentially unsafe storage directory: "${dir}". ` +
`Consider using a relative path for security.`);
}
const baseUrl = process.env.VOILA_STORAGE_BASE_URL;
if (baseUrl && !baseUrl.startsWith('/') && !isValidUrl(baseUrl)) {
throw new Error(`Invalid VOILA_STORAGE_BASE_URL: "${baseUrl}". Must be a path or valid URL`);
}
}
/**
* Validates production storage configuration
* @llm-rule WHEN: Running in production environment
* @llm-rule AVOID: Local storage in multi-server production - files won't sync across servers
*/
function validateProductionConfig() {
const strategy = detectStorageStrategy();
if (strategy === 'local') {
console.warn('[VoilaJSX AppKit] Using local storage in production. ' +
'Files will only exist on single server instance. ' +
'Set AWS_S3_BUCKET or CLOUDFLARE_R2_BUCKET for distributed storage.');
}
// Warn about missing CDN in production
const cdnUrl = process.env.VOILA_STORAGE_CDN_URL || process.env.CLOUDFLARE_R2_CDN_URL;
if (!cdnUrl && strategy !== 'local') {
console.warn('[VoilaJSX AppKit] No CDN URL configured in production. ' +
'Set VOILA_STORAGE_CDN_URL for better performance.');
}
}
/**
* Validates bucket name format (S3/R2 compatible)
*/
function isValidBucketName(name) {
if (name.length < 3 || name.length > 63)
return false;
if (name !== name.toLowerCase())
return false;
if (name.includes('..') || name.includes('.-') || name.includes('-.'))
return false;
if (name.startsWith('-') || name.endsWith('-'))
return false;
if (name.startsWith('.') || name.endsWith('.'))
return false;
return /^[a-z0-9.-]+$/.test(name);
}
/**
* Validates URL format
*/
function isValidUrl(url) {
try {
new URL(url);
return true;
}
catch {
return false;
}
}
/**
* Validates numeric environment variable within acceptable range
*/
function validateNumericEnv(name, min, max) {
const value = process.env[name];
if (!value)
return;
const num = parseInt(value);
if (isNaN(num) || num < min || num > max) {
throw new Error(`Invalid ${name}: "${value}". Must be a number between ${min} and ${max}`);
}
}
/**
* Gets storage configuration summary for debugging and health checks
* @llm-rule WHEN: Debugging storage configuration or building health check endpoints
* @llm-rule AVOID: Exposing sensitive connection details - this only shows safe info
*/
export function getConfigSummary() {
const config = getSmartDefaults();
return {
strategy: config.strategy,
local: config.strategy === 'local',
s3: config.strategy === 's3',
r2: config.strategy === 'r2',
environment: config.environment.nodeEnv,
};
}
/**
* Checks if cloud storage is available and properly configured
* @llm-rule WHEN: Conditional logic based on storage capabilities
* @llm-rule AVOID: Complex storage detection - just use storage normally, strategy handles it
*/
export function hasCloudStorage() {
const strategy = detectStorageStrategy();
return strategy === 's3' || strategy === 'r2';
}
/**
* Gets recommended configuration for different deployment types
* @llm-rule WHEN: Setting up storage for specific deployment scenarios
* @llm-rule AVOID: Default config for specialized deployments - needs specific tuning
*/
export function getDeploymentConfig(type) {
switch (type) {
case 'development':
return {
strategy: 'local',
local: {
dir: './uploads',
baseUrl: '/uploads',
maxFileSize: 10485760, // 10MB
allowedTypes: ['*'], // Allow all for development
createDirs: true,
},
};
case 'staging':
return {
strategy: hasCloudStorage() ? detectStorageStrategy() : 'local',
local: {
dir: './uploads-staging',
baseUrl: '/uploads',
maxFileSize: 26214400, // 25MB
allowedTypes: parseAllowedTypes(),
createDirs: true,
},
};
case 'production':
const strategy = detectStorageStrategy();
if (strategy === 'local') {
console.warn('[VoilaJSX AppKit] Local storage not recommended for production');
}
return {
strategy,
local: {
dir: './uploads-prod',
baseUrl: '/uploads',
maxFileSize: 52428800, // 50MB
allowedTypes: parseAllowedTypes(),
createDirs: false, // Don't auto-create in production
},
};
default:
throw new Error(`Unknown deployment type: ${type}`);
}
}
//# sourceMappingURL=defaults.js.map