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

Merge pull request #27 from skywatch-bsky/feat/env-validation

Feat/env validation

authored by Scarnecchia and committed by GitHub 5d8b296e 4574e337

+2 -1
.claude/settings.local.json
··· 9 9 "Bash(bun run lint:*)", 10 10 "mcp__git-mcp-server__git_commit", 11 11 "mcp__git-mcp-server__git_push", 12 - "Bash(bunx eslint:*)", 12 + "Bash(bunx tsc:*)", 13 + "Bash(DID=did:test OZONE_URL=http://test OZONE_PDS=http://test BSKY_HANDLE=test.bsky.social BSKY_PASSWORD=testpass bun --eval \"import(''./src/validateEnv.js'').then(m => m.validateEnvironment())\")", 13 14 "mcp__git-mcp-server__git_add" 14 15 ], 15 16 "ask": [
+1 -1
eslint.config.mjs
··· 83 83 84 84 // Style preferences 85 85 "@stylistic/indent": ["error", 2], 86 - "@stylistic/quotes": ["error", "single"], 86 + "@stylistic/quotes": ["error", "double"], 87 87 "@stylistic/semi": ["error", "always"], 88 88 //"@stylistic/comma-dangle": ["error", "es5"], 89 89 "@stylistic/object-curly-spacing": ["error", "always"],
+181 -106
src/main.ts
··· 1 - import fs from 'node:fs'; 1 + import fs from "node:fs"; 2 2 3 3 import type { 4 4 CommitCreateEvent, 5 5 CommitUpdateEvent, 6 - IdentityEvent } from '@skyware/jetstream'; 7 - import { 8 - Jetstream, 9 - } from '@skyware/jetstream'; 10 - 6 + IdentityEvent, 7 + } from "@skyware/jetstream"; 8 + import { Jetstream } from "@skyware/jetstream"; 11 9 12 - import { checkHandle } from './checkHandles.js'; 13 - import { checkPosts } from './checkPosts.js'; 14 - import { checkDescription, checkDisplayName } from './checkProfiles.js'; 15 - import { checkStarterPack, checkNewStarterPack } from './checkStarterPack.js'; 10 + import { checkHandle } from "./checkHandles.js"; 11 + import { checkPosts } from "./checkPosts.js"; 12 + import { checkDescription, checkDisplayName } from "./checkProfiles.js"; 13 + import { checkStarterPack, checkNewStarterPack } from "./checkStarterPack.js"; 16 14 import { 17 15 CURSOR_UPDATE_INTERVAL, 18 16 FIREHOSE_URL, 19 17 METRICS_PORT, 20 18 WANTED_COLLECTION, 21 - } from './config.js'; 22 - import logger from './logger.js'; 23 - import { startMetricsServer } from './metrics.js'; 24 - import type { Post, LinkFeature } from './types.js'; 19 + } from "./config.js"; 20 + import { validateEnvironment } from "./validateEnv.js"; 21 + import logger from "./logger.js"; 22 + import { startMetricsServer } from "./metrics.js"; 23 + import type { Post, LinkFeature } from "./types.js"; 24 + 25 + validateEnvironment(); 25 26 26 27 let cursor = 0; 27 28 let cursorUpdateInterval: NodeJS.Timeout; ··· 31 32 } 32 33 33 34 try { 34 - logger.info('Trying to read cursor from cursor.txt...'); 35 - cursor = Number(fs.readFileSync('cursor.txt', 'utf8')); 36 - logger.info(`Cursor found: ${cursor.toString()} (${epochUsToDateTime(cursor)})`); 35 + logger.info("Trying to read cursor from cursor.txt..."); 36 + cursor = Number(fs.readFileSync("cursor.txt", "utf8")); 37 + logger.info( 38 + `Cursor found: ${cursor.toString()} (${epochUsToDateTime(cursor)})`, 39 + ); 37 40 } catch (error) { 38 - if (error instanceof Error && 'code' in error && error.code === 'ENOENT') { 41 + if (error instanceof Error && "code" in error && error.code === "ENOENT") { 39 42 cursor = Math.floor(Date.now() * 1000); 40 43 logger.info( 41 44 `Cursor not found in cursor.txt, setting cursor to: ${cursor.toString()} (${epochUsToDateTime(cursor)})`, 42 45 ); 43 - fs.writeFileSync('cursor.txt', cursor.toString(), 'utf8'); 46 + fs.writeFileSync("cursor.txt", cursor.toString(), "utf8"); 44 47 } else { 45 48 logger.error(error); 46 49 process.exit(1); ··· 53 56 cursor, 54 57 }); 55 58 56 - jetstream.on('open', () => { 59 + jetstream.on("open", () => { 57 60 if (jetstream.cursor) { 58 61 logger.info( 59 62 `Connected to Jetstream at ${FIREHOSE_URL} with cursor ${jetstream.cursor.toString()} (${epochUsToDateTime(jetstream.cursor)})`, ··· 68 71 logger.info( 69 72 `Cursor updated to: ${jetstream.cursor.toString()} (${epochUsToDateTime(jetstream.cursor)})`, 70 73 ); 71 - fs.writeFile('cursor.txt', jetstream.cursor.toString(), (err) => { 74 + fs.writeFile("cursor.txt", jetstream.cursor.toString(), (err) => { 72 75 if (err) logger.error(err); 73 76 }); 74 77 } 75 78 }, CURSOR_UPDATE_INTERVAL); 76 79 }); 77 80 78 - jetstream.on('close', () => { 81 + jetstream.on("close", () => { 79 82 clearInterval(cursorUpdateInterval); 80 - logger.info('Jetstream connection closed.'); 83 + logger.info("Jetstream connection closed."); 81 84 }); 82 85 83 - jetstream.on('error', (error) => { 86 + jetstream.on("error", (error) => { 84 87 logger.error(`Jetstream error: ${error.message}`); 85 88 }); 86 89 87 90 // Check for post updates 88 91 89 92 jetstream.onCreate( 90 - 'app.bsky.feed.post', 91 - (event: CommitCreateEvent<'app.bsky.feed.post'>) => { 93 + "app.bsky.feed.post", 94 + (event: CommitCreateEvent<"app.bsky.feed.post">) => { 92 95 try { 93 96 const atURI = `at://${event.did}/app.bsky.feed.post/${event.commit.rkey}`; 94 - const hasFacets = Object.hasOwn(event.commit.record, 'facets'); 95 - const hasText = Object.hasOwn(event.commit.record, 'text'); 97 + const hasFacets = Object.hasOwn(event.commit.record, "facets"); 98 + const hasText = Object.hasOwn(event.commit.record, "text"); 96 99 97 100 const tasks: Promise<void>[] = []; 98 101 ··· 100 103 if (hasFacets && event.commit.record.facets) { 101 104 const hasLinkType = event.commit.record.facets.some((facet) => 102 105 facet.features.some( 103 - (feature) => feature.$type === 'app.bsky.richtext.facet#link', 106 + (feature) => feature.$type === "app.bsky.richtext.facet#link", 104 107 ), 105 108 ); 106 109 107 110 if (hasLinkType) { 108 - const urls = event.commit.record.facets.flatMap((facet) => 109 - facet.features.filter( 110 - (feature) => feature.$type === 'app.bsky.richtext.facet#link', 111 - ), 112 - ) 111 + const urls = event.commit.record.facets 112 + .flatMap((facet) => 113 + facet.features.filter( 114 + (feature) => feature.$type === "app.bsky.richtext.facet#link", 115 + ), 116 + ) 113 117 .map((feature: LinkFeature) => feature.uri); 114 118 115 119 urls.forEach((url) => { ··· 123 127 cid: event.commit.cid, 124 128 }, 125 129 ]; 126 - tasks.push(checkPosts(posts).catch((error: unknown) => { 127 - logger.error(`Error checking post links for ${event.did}:`, error); 128 - })); 130 + tasks.push( 131 + checkPosts(posts).catch((error: unknown) => { 132 + logger.error( 133 + `Error checking post links for ${event.did}:`, 134 + error, 135 + ); 136 + }), 137 + ); 129 138 }); 130 139 } 131 140 } else if (hasText && event.commit.record.text) { ··· 139 148 cid: event.commit.cid, 140 149 }, 141 150 ]; 142 - tasks.push(checkPosts(posts).catch((error: unknown) => { 143 - logger.error(`Error checking post text for ${event.did}:`, error); 144 - })); 151 + tasks.push( 152 + checkPosts(posts).catch((error: unknown) => { 153 + logger.error(`Error checking post text for ${event.did}:`, error); 154 + }), 155 + ); 145 156 } 146 157 147 158 // Wait for all tasks to complete ··· 156 167 157 168 // Check for profile updates 158 169 jetstream.onUpdate( 159 - 'app.bsky.actor.profile', 160 - (event: CommitUpdateEvent<'app.bsky.actor.profile'>) => { 170 + "app.bsky.actor.profile", 171 + (event: CommitUpdateEvent<"app.bsky.actor.profile">) => { 161 172 try { 162 173 const tasks: Promise<void>[] = []; 163 174 164 175 if (event.commit.record.displayName || event.commit.record.description) { 165 - const displayName = event.commit.record.displayName ?? ''; 166 - const description = event.commit.record.description ?? ''; 167 - 176 + const displayName = event.commit.record.displayName ?? ""; 177 + const description = event.commit.record.description ?? ""; 178 + 168 179 tasks.push( 169 - checkDescription(event.did, event.time_us, displayName, description) 170 - .catch((error: unknown) => { 171 - logger.error(`Error checking profile description for ${event.did}:`, error); 172 - }) 180 + checkDescription( 181 + event.did, 182 + event.time_us, 183 + displayName, 184 + description, 185 + ).catch((error: unknown) => { 186 + logger.error( 187 + `Error checking profile description for ${event.did}:`, 188 + error, 189 + ); 190 + }), 173 191 ); 174 - 192 + 175 193 tasks.push( 176 - checkDisplayName(event.did, event.time_us, displayName, description) 177 - .catch((error: unknown) => { 178 - logger.error(`Error checking profile display name for ${event.did}:`, error); 179 - }) 194 + checkDisplayName( 195 + event.did, 196 + event.time_us, 197 + displayName, 198 + description, 199 + ).catch((error: unknown) => { 200 + logger.error( 201 + `Error checking profile display name for ${event.did}:`, 202 + error, 203 + ); 204 + }), 180 205 ); 181 206 } 182 207 183 208 if (event.commit.record.joinedViaStarterPack) { 184 209 tasks.push( 185 - checkStarterPack(event.did, event.time_us, event.commit.record.joinedViaStarterPack.uri) 186 - .catch((error: unknown) => { 187 - logger.error(`Error checking starter pack for ${event.did}:`, error); 188 - }) 210 + checkStarterPack( 211 + event.did, 212 + event.time_us, 213 + event.commit.record.joinedViaStarterPack.uri, 214 + ).catch((error: unknown) => { 215 + logger.error( 216 + `Error checking starter pack for ${event.did}:`, 217 + error, 218 + ); 219 + }), 189 220 ); 190 221 } 191 222 ··· 194 225 void Promise.allSettled(tasks); 195 226 } 196 227 } catch (error: unknown) { 197 - logger.error(`Error processing profile update event for ${event.did}:`, error); 228 + logger.error( 229 + `Error processing profile update event for ${event.did}:`, 230 + error, 231 + ); 198 232 } 199 233 }, 200 234 ); ··· 202 236 // Check for profile updates 203 237 204 238 jetstream.onCreate( 205 - 'app.bsky.actor.profile', 206 - (event: CommitCreateEvent<'app.bsky.actor.profile'>) => { 239 + "app.bsky.actor.profile", 240 + (event: CommitCreateEvent<"app.bsky.actor.profile">) => { 207 241 try { 208 242 const tasks: Promise<void>[] = []; 209 243 210 244 if (event.commit.record.displayName || event.commit.record.description) { 211 - const displayName = event.commit.record.displayName ?? ''; 212 - const description = event.commit.record.description ?? ''; 213 - 245 + const displayName = event.commit.record.displayName ?? ""; 246 + const description = event.commit.record.description ?? ""; 247 + 214 248 tasks.push( 215 - checkDescription(event.did, event.time_us, displayName, description) 216 - .catch((error: unknown) => { 217 - logger.error(`Error checking profile description for ${event.did}:`, error); 218 - }) 249 + checkDescription( 250 + event.did, 251 + event.time_us, 252 + displayName, 253 + description, 254 + ).catch((error: unknown) => { 255 + logger.error( 256 + `Error checking profile description for ${event.did}:`, 257 + error, 258 + ); 259 + }), 219 260 ); 220 - 261 + 221 262 tasks.push( 222 - checkDisplayName(event.did, event.time_us, displayName, description) 223 - .catch((error: unknown) => { 224 - logger.error(`Error checking profile display name for ${event.did}:`, error); 225 - }) 263 + checkDisplayName( 264 + event.did, 265 + event.time_us, 266 + displayName, 267 + description, 268 + ).catch((error: unknown) => { 269 + logger.error( 270 + `Error checking profile display name for ${event.did}:`, 271 + error, 272 + ); 273 + }), 226 274 ); 227 275 228 276 if (event.commit.record.joinedViaStarterPack) { 229 277 tasks.push( 230 - checkStarterPack(event.did, event.time_us, event.commit.record.joinedViaStarterPack.uri) 231 - .catch((error: unknown) => { 232 - logger.error(`Error checking starter pack for ${event.did}:`, error); 233 - }) 278 + checkStarterPack( 279 + event.did, 280 + event.time_us, 281 + event.commit.record.joinedViaStarterPack.uri, 282 + ).catch((error: unknown) => { 283 + logger.error( 284 + `Error checking starter pack for ${event.did}:`, 285 + error, 286 + ); 287 + }), 234 288 ); 235 289 } 236 290 ··· 240 294 } 241 295 } 242 296 } catch (error: unknown) { 243 - logger.error(`Error processing profile creation event for ${event.did}:`, error); 297 + logger.error( 298 + `Error processing profile creation event for ${event.did}:`, 299 + error, 300 + ); 244 301 } 245 302 }, 246 303 ); 247 304 248 305 jetstream.onCreate( 249 - 'app.bsky.graph.starterpack', 250 - (event: CommitCreateEvent<'app.bsky.graph.starterpack'>) => { 306 + "app.bsky.graph.starterpack", 307 + (event: CommitCreateEvent<"app.bsky.graph.starterpack">) => { 251 308 try { 252 309 const atURI = `at://${event.did}/app.bsky.feed.post/${event.commit.rkey}`; 253 310 const { name, description } = event.commit.record; ··· 260 317 name, 261 318 description, 262 319 ).catch((error: unknown) => { 263 - logger.error(`Error checking new starter pack for ${event.did}:`, error); 320 + logger.error( 321 + `Error checking new starter pack for ${event.did}:`, 322 + error, 323 + ); 264 324 }); 265 325 } catch (error: unknown) { 266 - logger.error(`Error processing starter pack creation event for ${event.did}:`, error); 326 + logger.error( 327 + `Error processing starter pack creation event for ${event.did}:`, 328 + error, 329 + ); 267 330 } 268 331 }, 269 332 ); 270 333 271 334 jetstream.onUpdate( 272 - 'app.bsky.graph.starterpack', 273 - (event: CommitUpdateEvent<'app.bsky.graph.starterpack'>) => { 335 + "app.bsky.graph.starterpack", 336 + (event: CommitUpdateEvent<"app.bsky.graph.starterpack">) => { 274 337 try { 275 338 const atURI = `at://${event.did}/app.bsky.feed.post/${event.commit.rkey}`; 276 339 const { name, description } = event.commit.record; ··· 283 346 name, 284 347 description, 285 348 ).catch((error: unknown) => { 286 - logger.error(`Error checking updated starter pack for ${event.did}:`, error); 349 + logger.error( 350 + `Error checking updated starter pack for ${event.did}:`, 351 + error, 352 + ); 287 353 }); 288 354 } catch (error: unknown) { 289 - logger.error(`Error processing starter pack update event for ${event.did}:`, error); 355 + logger.error( 356 + `Error processing starter pack update event for ${event.did}:`, 357 + error, 358 + ); 290 359 } 291 360 }, 292 361 ); 293 362 294 363 // Check for handle updates 295 - jetstream.on('identity', (event: IdentityEvent) => { 364 + jetstream.on("identity", (event: IdentityEvent) => { 296 365 try { 297 366 if (event.identity.handle) { 298 - void checkHandle(event.identity.did, event.identity.handle, event.time_us) 299 - .catch((error: unknown) => { 300 - logger.error(`Error checking handle for ${event.identity.did}:`, error); 301 - }); 367 + void checkHandle( 368 + event.identity.did, 369 + event.identity.handle, 370 + event.time_us, 371 + ).catch((error: unknown) => { 372 + logger.error(`Error checking handle for ${event.identity.did}:`, error); 373 + }); 302 374 } 303 375 } catch (error: unknown) { 304 - logger.error(`Error processing identity event for ${event.identity.did}:`, error); 376 + logger.error( 377 + `Error processing identity event for ${event.identity.did}:`, 378 + error, 379 + ); 305 380 } 306 381 }); 307 382 ··· 311 386 metricsServer = startMetricsServer(METRICS_PORT); 312 387 logger.info(`Metrics server started on port ${METRICS_PORT.toString()}`); 313 388 } catch (error: unknown) { 314 - logger.error('Failed to start metrics server:', error); 389 + logger.error("Failed to start metrics server:", error); 315 390 process.exit(1); 316 391 } 317 392 ··· 326 401 // Start jetstream with error handling 327 402 try { 328 403 jetstream.start(); 329 - logger.info('Jetstream started successfully'); 404 + logger.info("Jetstream started successfully"); 330 405 } catch (error: unknown) { 331 - logger.error('Failed to start jetstream:', error); 406 + logger.error("Failed to start jetstream:", error); 332 407 process.exit(1); 333 408 } 334 409 335 410 function shutdown() { 336 411 try { 337 - logger.info('Shutting down gracefully...'); 412 + logger.info("Shutting down gracefully..."); 338 413 if (jetstream.cursor) { 339 - fs.writeFileSync('cursor.txt', jetstream.cursor.toString(), 'utf8'); 414 + fs.writeFileSync("cursor.txt", jetstream.cursor.toString(), "utf8"); 340 415 } 341 416 jetstream.close(); 342 417 if (metricsServer) { 343 418 metricsServer.close(() => { 344 - logger.info('Metrics server closed'); 419 + logger.info("Metrics server closed"); 345 420 }); 346 421 } 347 - logger.info('Shutdown completed successfully'); 422 + logger.info("Shutdown completed successfully"); 348 423 } catch (error: unknown) { 349 - logger.error('Error shutting down gracefully:', error); 424 + logger.error("Error shutting down gracefully:", error); 350 425 process.exit(1); 351 426 } 352 427 } 353 428 354 429 // Global error handlers 355 - process.on('unhandledRejection', (reason, promise) => { 356 - logger.error('Unhandled Promise Rejection at:', promise, 'reason:', reason); 430 + process.on("unhandledRejection", (reason, promise) => { 431 + logger.error("Unhandled Promise Rejection at:", promise, "reason:", reason); 357 432 // Don't exit the process for unhandled rejections, just log them 358 433 }); 359 434 360 - process.on('uncaughtException', (error) => { 361 - logger.error('Uncaught Exception:', error); 435 + process.on("uncaughtException", (error) => { 436 + logger.error("Uncaught Exception:", error); 362 437 shutdown(); 363 438 }); 364 439 365 - process.on('SIGINT', shutdown); 366 - process.on('SIGTERM', shutdown); 440 + process.on("SIGINT", shutdown); 441 + process.on("SIGTERM", shutdown);
+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 + }