+2
-1
.claude/settings.local.json
+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
+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
+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
+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
+
}