A tool for parsing traffic on the jetstream and applying a moderation workstream based on regexp based rules

feat: add comprehensive environment variable validation at startup

- Add validateEnv.ts with validation for all required and optional environment variables
- Validate DID format (must start with 'did:')
- Validate OZONE_URL and OZONE_PDS as domain names
- Validate BSKY_HANDLE format (must contain '.')
- Support arithmetic expressions for LABEL_LIMIT and LABEL_LIMIT_WAIT (e.g., "2900 * 1000")
- Validate numeric values for ports and intervals
- Validate LOG_LEVEL and NODE_ENV against allowed values
- Exit with code 1 and clear error messages if validation fails
- Integrated validation call at application startup in main.ts

This addresses the security concern of missing environment variable validation identified in the code review.

+168
+3
src/main.ts
··· 15 15 } from "./config.js"; 16 16 import logger from "./logger.js"; 17 17 import { startMetricsServer } from "./metrics.js"; 18 + import { validateEnvironment } from "./validateEnv.js"; 18 19 import { Post, LinkFeature, Handle } from "./types.js"; 19 20 import { checkPosts } from "./checkPosts.js"; 20 21 import { checkHandle } from "./checkHandles.js"; 21 22 import { checkStarterPack, checkNewStarterPack } from "./checkStarterPack.js"; 22 23 import { checkDescription, checkDisplayName } from "./checkProfiles.js"; 24 + 25 + validateEnvironment(); 23 26 24 27 let cursor = 0; 25 28 let cursorUpdateInterval: NodeJS.Timeout;
+165
src/validateEnv.ts
··· 1 + import logger from './logger.js'; 2 + 3 + interface EnvironmentVariable { 4 + name: string; 5 + required: boolean; 6 + description: string; 7 + validator?: (value: string) => boolean; 8 + } 9 + 10 + const ENV_VARIABLES: EnvironmentVariable[] = [ 11 + { 12 + name: 'DID', 13 + required: true, 14 + description: 'Moderator DID for labeling operations', 15 + validator: (value) => value.startsWith('did:'), 16 + }, 17 + { 18 + name: 'OZONE_URL', 19 + required: true, 20 + description: 'Ozone server URL', 21 + validator: (value) => value.includes('.') && value.length > 3, 22 + }, 23 + { 24 + name: 'OZONE_PDS', 25 + required: true, 26 + description: 'Ozone PDS URL', 27 + validator: (value) => value.includes('.') && value.length > 3, 28 + }, 29 + { 30 + name: 'BSKY_HANDLE', 31 + required: true, 32 + description: 'Bluesky handle for authentication', 33 + validator: (value) => value.includes('.'), 34 + }, 35 + { 36 + name: 'BSKY_PASSWORD', 37 + required: true, 38 + description: 'Bluesky password for authentication', 39 + validator: (value) => value.length > 0, 40 + }, 41 + { 42 + name: 'HOST', 43 + required: false, 44 + description: 'Host address for the server (defaults to 127.0.0.1)', 45 + }, 46 + { 47 + name: 'PORT', 48 + required: false, 49 + description: 'Port for the main server (defaults to 4100)', 50 + validator: (value) => !isNaN(Number(value)) && Number(value) > 0, 51 + }, 52 + { 53 + name: 'METRICS_PORT', 54 + required: false, 55 + description: 'Port for metrics server (defaults to 4101)', 56 + validator: (value) => !isNaN(Number(value)) && Number(value) > 0, 57 + }, 58 + { 59 + name: 'FIREHOSE_URL', 60 + required: false, 61 + description: 'Jetstream firehose WebSocket URL', 62 + validator: (value) => value.startsWith('ws'), 63 + }, 64 + { 65 + name: 'CURSOR_UPDATE_INTERVAL', 66 + required: false, 67 + description: 'Cursor update interval in milliseconds (defaults to 60000)', 68 + validator: (value) => !isNaN(Number(value)) && Number(value) > 0, 69 + }, 70 + { 71 + name: 'LABEL_LIMIT', 72 + required: false, 73 + description: 'Rate limit for labeling operations', 74 + validator: (value) => { 75 + // Allow "number * number" format or plain numbers 76 + const multiplyMatch = /^(\d+)\s*\*\s*(\d+)$/.exec(value); 77 + if (multiplyMatch) { 78 + const result = Number(multiplyMatch[1]) * Number(multiplyMatch[2]); 79 + return result > 0; 80 + } 81 + return !isNaN(Number(value)) && Number(value) > 0; 82 + }, 83 + }, 84 + { 85 + name: 'LABEL_LIMIT_WAIT', 86 + required: false, 87 + description: 'Wait time between rate limited operations', 88 + validator: (value) => { 89 + // Allow "number * number" format or plain numbers 90 + const multiplyMatch = /^(\d+)\s*\*\s*(\d+)$/.exec(value); 91 + if (multiplyMatch) { 92 + const result = Number(multiplyMatch[1]) * Number(multiplyMatch[2]); 93 + return result > 0; 94 + } 95 + return !isNaN(Number(value)) && Number(value) > 0; 96 + }, 97 + }, 98 + { 99 + name: 'LOG_LEVEL', 100 + required: false, 101 + description: 'Logging level (trace, debug, info, warn, error, fatal)', 102 + validator: (value) => 103 + ['trace', 'debug', 'info', 'warn', 'error', 'fatal'].includes(value), 104 + }, 105 + { 106 + name: 'NODE_ENV', 107 + required: false, 108 + description: 'Node environment (development, production, test)', 109 + validator: (value) => ['development', 'production', 'test'].includes(value), 110 + }, 111 + ]; 112 + 113 + export function validateEnvironment(): void { 114 + const errors: string[] = []; 115 + const warnings: string[] = []; 116 + 117 + logger.info('Validating environment variables...'); 118 + 119 + for (const envVar of ENV_VARIABLES) { 120 + const value = process.env[envVar.name]; 121 + 122 + if (envVar.required) { 123 + if (!value || value.trim() === '') { 124 + errors.push( 125 + `Required environment variable ${envVar.name} is missing. ${envVar.description}` 126 + ); 127 + continue; 128 + } 129 + } 130 + 131 + if (value && envVar.validator) { 132 + try { 133 + if (!envVar.validator(value)) { 134 + errors.push( 135 + `Environment variable ${envVar.name} has invalid format. ${envVar.description}` 136 + ); 137 + } 138 + } catch (error) { 139 + errors.push( 140 + `Environment variable ${envVar.name} validation failed: ${String(error)}. ${envVar.description}` 141 + ); 142 + } 143 + } 144 + 145 + if (!envVar.required && !value) { 146 + warnings.push( 147 + `Optional environment variable ${envVar.name} not set, using default. ${envVar.description}` 148 + ); 149 + } 150 + } 151 + 152 + if (warnings.length > 0) { 153 + logger.warn('Environment variable warnings:'); 154 + warnings.forEach((warning) => { logger.warn(` - ${warning}`); }); 155 + } 156 + 157 + if (errors.length > 0) { 158 + logger.error('Environment variable validation failed:'); 159 + errors.forEach((error) => { logger.error(` - ${error}`); }); 160 + logger.error('Please check your environment configuration and try again.'); 161 + process.exit(1); 162 + } 163 + 164 + logger.info('Environment variable validation completed successfully'); 165 + }