@friggframework/core 2.0.0-next.4 → 2.0.0-next.41

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,844 @@
1
+ const { Router } = require('express');
2
+ const mongoose = require('mongoose');
3
+ const https = require('https');
4
+ const http = require('http');
5
+ const { moduleFactory, integrationFactory } = require('./../backend-utils');
6
+ const { createAppHandler } = require('./../app-handler-helpers');
7
+
8
+ const router = Router();
9
+
10
+ const validateApiKey = (req, res, next) => {
11
+ const apiKey = req.headers['x-api-key'];
12
+
13
+ if (req.path === '/health') {
14
+ return next();
15
+ }
16
+
17
+ if (!apiKey || apiKey !== process.env.HEALTH_API_KEY) {
18
+ console.error('Unauthorized access attempt to health endpoint');
19
+ return res.status(401).json({
20
+ status: 'error',
21
+ message: 'Unauthorized',
22
+ });
23
+ }
24
+
25
+ next();
26
+ };
27
+
28
+ router.use(validateApiKey);
29
+
30
+ const checkExternalAPI = (url, timeout = 5000) => {
31
+ return new Promise((resolve) => {
32
+ const protocol = url.startsWith('https:') ? https : http;
33
+ const startTime = Date.now();
34
+
35
+ try {
36
+ const request = protocol.get(url, { timeout }, (res) => {
37
+ const responseTime = Date.now() - startTime;
38
+ resolve({
39
+ status: 'healthy',
40
+ statusCode: res.statusCode,
41
+ responseTime,
42
+ reachable: res.statusCode < 500,
43
+ });
44
+ });
45
+
46
+ request.on('error', (error) => {
47
+ resolve({
48
+ status: 'unhealthy',
49
+ error: error.message,
50
+ responseTime: Date.now() - startTime,
51
+ reachable: false,
52
+ });
53
+ });
54
+
55
+ request.on('timeout', () => {
56
+ request.destroy();
57
+ resolve({
58
+ status: 'timeout',
59
+ error: 'Request timeout',
60
+ responseTime: timeout,
61
+ reachable: false,
62
+ });
63
+ });
64
+ } catch (error) {
65
+ resolve({
66
+ status: 'error',
67
+ error: error.message,
68
+ responseTime: Date.now() - startTime,
69
+ reachable: false,
70
+ });
71
+ }
72
+ });
73
+ };
74
+
75
+ const getDatabaseState = () => {
76
+ const stateMap = {
77
+ 0: 'disconnected',
78
+ 1: 'connected',
79
+ 2: 'connecting',
80
+ 3: 'disconnecting',
81
+ };
82
+ const readyState = mongoose.connection.readyState;
83
+
84
+ return {
85
+ readyState,
86
+ stateName: stateMap[readyState],
87
+ isConnected: readyState === 1,
88
+ };
89
+ };
90
+
91
+ const checkDatabaseHealth = async () => {
92
+ const { stateName, isConnected } = getDatabaseState();
93
+ const result = {
94
+ status: isConnected ? 'healthy' : 'unhealthy',
95
+ state: stateName,
96
+ };
97
+
98
+ if (isConnected) {
99
+ const pingStart = Date.now();
100
+ await mongoose.connection.db.admin().ping({ maxTimeMS: 2000 });
101
+ result.responseTime = Date.now() - pingStart;
102
+ }
103
+
104
+ return result;
105
+ };
106
+
107
+ const getEncryptionConfiguration = () => {
108
+ const { STAGE, BYPASS_ENCRYPTION_STAGE, KMS_KEY_ARN, AES_KEY_ID } =
109
+ process.env;
110
+
111
+ const defaultBypassStages = ['dev', 'test', 'local'];
112
+ const useEnv = BYPASS_ENCRYPTION_STAGE !== undefined;
113
+ const bypassStages = useEnv
114
+ ? BYPASS_ENCRYPTION_STAGE.split(',').map((s) => s.trim())
115
+ : defaultBypassStages;
116
+
117
+ const isBypassed = bypassStages.includes(STAGE);
118
+ const hasAES = AES_KEY_ID && AES_KEY_ID.trim() !== '';
119
+ const hasKMS = KMS_KEY_ARN && KMS_KEY_ARN.trim() !== '' && !hasAES;
120
+ const mode = hasAES ? 'aes' : hasKMS ? 'kms' : 'none';
121
+
122
+ return {
123
+ stage: STAGE || null,
124
+ isBypassed,
125
+ hasAES,
126
+ hasKMS,
127
+ mode,
128
+ };
129
+ };
130
+
131
+ const createTestEncryptionModel = () => {
132
+ const { Encrypt } = require('./../../encrypt');
133
+
134
+ const testSchema = new mongoose.Schema(
135
+ {
136
+ testSecret: { type: String, lhEncrypt: true },
137
+ normalField: { type: String },
138
+ nestedSecret: {
139
+ value: { type: String, lhEncrypt: true },
140
+ },
141
+ },
142
+ { timestamps: false }
143
+ );
144
+
145
+ testSchema.plugin(Encrypt);
146
+
147
+ return (
148
+ mongoose.models.TestEncryption ||
149
+ mongoose.model('TestEncryption', testSchema)
150
+ );
151
+ };
152
+
153
+ const verifyDecryption = (retrievedDoc, originalData) => {
154
+ return (
155
+ retrievedDoc &&
156
+ retrievedDoc.testSecret === originalData.testSecret &&
157
+ retrievedDoc.normalField === originalData.normalField &&
158
+ retrievedDoc.nestedSecret?.value === originalData.nestedSecret.value
159
+ );
160
+ };
161
+
162
+ const verifyEncryptionInDatabase = async (testDoc, originalData, TestModel) => {
163
+ const collectionName = TestModel.collection.name;
164
+ const rawDoc = await mongoose.connection.db
165
+ .collection(collectionName)
166
+ .findOne({ _id: testDoc._id });
167
+
168
+ const secretIsEncrypted =
169
+ rawDoc &&
170
+ typeof rawDoc.testSecret === 'string' &&
171
+ rawDoc.testSecret.includes(':') &&
172
+ rawDoc.testSecret !== originalData.testSecret;
173
+
174
+ const nestedIsEncrypted =
175
+ rawDoc?.nestedSecret?.value &&
176
+ typeof rawDoc.nestedSecret.value === 'string' &&
177
+ rawDoc.nestedSecret.value.includes(':') &&
178
+ rawDoc.nestedSecret.value !== originalData.nestedSecret.value;
179
+
180
+ const normalNotEncrypted =
181
+ rawDoc && rawDoc.normalField === originalData.normalField;
182
+
183
+ return {
184
+ secretIsEncrypted,
185
+ nestedIsEncrypted,
186
+ normalNotEncrypted,
187
+ };
188
+ };
189
+
190
+ const evaluateEncryptionTestResults = (decryptionWorks, encryptionResults) => {
191
+ const { secretIsEncrypted, nestedIsEncrypted, normalNotEncrypted } =
192
+ encryptionResults;
193
+
194
+ if (
195
+ decryptionWorks &&
196
+ secretIsEncrypted &&
197
+ nestedIsEncrypted &&
198
+ normalNotEncrypted
199
+ ) {
200
+ return {
201
+ status: 'enabled',
202
+ testResult: 'Encryption and decryption verified successfully',
203
+ };
204
+ }
205
+
206
+ if (decryptionWorks && (!secretIsEncrypted || !nestedIsEncrypted)) {
207
+ return {
208
+ status: 'unhealthy',
209
+ testResult: 'Fields are not being encrypted in database',
210
+ };
211
+ }
212
+
213
+ if (decryptionWorks && !normalNotEncrypted) {
214
+ return {
215
+ status: 'unhealthy',
216
+ testResult: 'Normal fields are being incorrectly encrypted',
217
+ };
218
+ }
219
+
220
+ return {
221
+ status: 'unhealthy',
222
+ testResult: 'Decryption failed or data mismatch',
223
+ };
224
+ };
225
+
226
+ const withTimeout = (promise, ms, errorMessage) => {
227
+ return Promise.race([
228
+ promise,
229
+ new Promise((_, reject) =>
230
+ setTimeout(() => reject(new Error(errorMessage)), ms)
231
+ ),
232
+ ]);
233
+ };
234
+
235
+ const testEncryption = async () => {
236
+ // eslint-disable-next-line no-console
237
+ console.log('Starting encryption test');
238
+ const TestModel = createTestEncryptionModel();
239
+ // eslint-disable-next-line no-console
240
+ console.log('Test model created');
241
+
242
+ const testData = {
243
+ testSecret: 'This is a secret value that should be encrypted',
244
+ normalField: 'This is a normal field that should not be encrypted',
245
+ nestedSecret: {
246
+ value: 'This is a nested secret that should be encrypted',
247
+ },
248
+ };
249
+
250
+ const testDoc = new TestModel(testData);
251
+ await withTimeout(testDoc.save(), 5000, 'Save operation timed out');
252
+ // eslint-disable-next-line no-console
253
+ console.log('Test document saved');
254
+
255
+ try {
256
+ const retrievedDoc = await withTimeout(
257
+ TestModel.findById(testDoc._id),
258
+ 5000,
259
+ 'Find operation timed out'
260
+ );
261
+ // eslint-disable-next-line no-console
262
+ console.log('Test document retrieved');
263
+ const decryptionWorks = verifyDecryption(retrievedDoc, testData);
264
+ const encryptionResults = await withTimeout(
265
+ verifyEncryptionInDatabase(testDoc, testData, TestModel),
266
+ 5000,
267
+ 'Database verification timed out'
268
+ );
269
+ // eslint-disable-next-line no-console
270
+ console.log('Encryption verification completed');
271
+
272
+ const evaluation = evaluateEncryptionTestResults(
273
+ decryptionWorks,
274
+ encryptionResults
275
+ );
276
+
277
+ return {
278
+ ...evaluation,
279
+ encryptionWorks: decryptionWorks,
280
+ };
281
+ } finally {
282
+ await withTimeout(
283
+ TestModel.deleteOne({ _id: testDoc._id }),
284
+ 5000,
285
+ 'Delete operation timed out'
286
+ );
287
+ // eslint-disable-next-line no-console
288
+ console.log('Test document deleted');
289
+ }
290
+ };
291
+
292
+ const checkEncryptionHealth = async () => {
293
+ const config = getEncryptionConfiguration();
294
+
295
+ if (config.isBypassed || config.mode === 'none') {
296
+ // eslint-disable-next-line no-console
297
+ console.log('Encryption check bypassed:', {
298
+ stage: config.stage,
299
+ mode: config.mode,
300
+ });
301
+
302
+ const testResult = config.isBypassed
303
+ ? 'Encryption bypassed for this stage'
304
+ : 'No encryption keys configured';
305
+
306
+ return {
307
+ status: 'disabled',
308
+ mode: config.mode,
309
+ bypassed: config.isBypassed,
310
+ stage: config.stage,
311
+ testResult,
312
+ encryptionWorks: false,
313
+ debug: {
314
+ hasKMS: config.hasKMS,
315
+ hasAES: config.hasAES,
316
+ },
317
+ };
318
+ }
319
+
320
+ try {
321
+ const testResults = await testEncryption();
322
+
323
+ return {
324
+ ...testResults,
325
+ mode: config.mode,
326
+ bypassed: config.isBypassed,
327
+ stage: config.stage,
328
+ debug: {
329
+ hasKMS: config.hasKMS,
330
+ hasAES: config.hasAES,
331
+ },
332
+ };
333
+ } catch (error) {
334
+ return {
335
+ status: 'unhealthy',
336
+ mode: config.mode,
337
+ bypassed: config.isBypassed,
338
+ stage: config.stage,
339
+ testResult: `Encryption test failed: ${error.message}`,
340
+ encryptionWorks: false,
341
+ debug: {
342
+ hasKMS: config.hasKMS,
343
+ hasAES: config.hasAES,
344
+ },
345
+ };
346
+ }
347
+ };
348
+
349
+ const checkExternalAPIs = async () => {
350
+ const apis = [
351
+ { name: 'github', url: 'https://api.github.com/status' },
352
+ { name: 'npm', url: 'https://registry.npmjs.org' },
353
+ ];
354
+
355
+ const results = await Promise.all(
356
+ apis.map((api) =>
357
+ checkExternalAPI(api.url).then((result) => ({
358
+ name: api.name,
359
+ ...result,
360
+ }))
361
+ )
362
+ );
363
+
364
+ const apiStatuses = {};
365
+ let allReachable = true;
366
+
367
+ results.forEach(({ name, ...checkResult }) => {
368
+ apiStatuses[name] = checkResult;
369
+ if (!checkResult.reachable) {
370
+ allReachable = false;
371
+ }
372
+ });
373
+
374
+ return { apiStatuses, allReachable };
375
+ };
376
+
377
+ const checkIntegrations = () => {
378
+ const moduleTypes = Array.isArray(moduleFactory.moduleTypes)
379
+ ? moduleFactory.moduleTypes
380
+ : [];
381
+
382
+ const integrationTypes = Array.isArray(integrationFactory.integrationTypes)
383
+ ? integrationFactory.integrationTypes
384
+ : [];
385
+
386
+ return {
387
+ status: 'healthy',
388
+ modules: {
389
+ count: moduleTypes.length,
390
+ available: moduleTypes,
391
+ },
392
+ integrations: {
393
+ count: integrationTypes.length,
394
+ available: integrationTypes,
395
+ },
396
+ };
397
+ };
398
+
399
+ const buildHealthCheckResponse = (startTime) => {
400
+ return {
401
+ service: 'frigg-core-api',
402
+ status: 'healthy',
403
+ timestamp: new Date().toISOString(),
404
+ checks: {},
405
+ calculateResponseTime: () => Date.now() - startTime,
406
+ };
407
+ };
408
+
409
+ // Helper to detect VPC configuration
410
+ const detectVpcConfiguration = async () => {
411
+ const results = {
412
+ isInVpc: false,
413
+ hasInternetAccess: false,
414
+ canResolvePublicDns: false,
415
+ canConnectToAws: false,
416
+ vpcEndpoints: [],
417
+ };
418
+
419
+ try {
420
+ // Check if we're in a VPC by looking for VPC-specific environment
421
+ // Lambda in VPC has specific network interface configuration
422
+ const dns = require('dns').promises;
423
+
424
+ // Test 1: Can we resolve public DNS? (indicates DNS configuration)
425
+ try {
426
+ await Promise.race([
427
+ dns.resolve4('www.google.com'),
428
+ new Promise((_, reject) =>
429
+ setTimeout(() => reject(new Error('timeout')), 2000)
430
+ ),
431
+ ]);
432
+ results.canResolvePublicDns = true;
433
+ } catch (e) {
434
+ console.log('Public DNS resolution failed:', e.message);
435
+ }
436
+
437
+ // Test 2: Can we reach internet? (indicates NAT gateway)
438
+ try {
439
+ const https = require('https');
440
+ await new Promise((resolve, reject) => {
441
+ const req = https.get(
442
+ 'https://www.google.com',
443
+ { timeout: 2000 },
444
+ (res) => {
445
+ res.destroy();
446
+ resolve(true);
447
+ }
448
+ );
449
+ req.on('error', reject);
450
+ req.on('timeout', () => {
451
+ req.destroy();
452
+ reject(new Error('timeout'));
453
+ });
454
+ });
455
+ results.hasInternetAccess = true;
456
+ } catch (e) {
457
+ console.log('Internet connectivity test failed:', e.message);
458
+ }
459
+
460
+ // Test 3: Check for VPC endpoints by trying to resolve internal AWS endpoints
461
+ const region = process.env.AWS_REGION; // Lambda always provides this
462
+ const vpcEndpointDomains = [
463
+ `com.amazonaws.${region}.kms`,
464
+ `com.amazonaws.vpce.${region}`,
465
+ `kms.${region}.amazonaws.com`,
466
+ ];
467
+
468
+ for (const domain of vpcEndpointDomains) {
469
+ try {
470
+ const addresses = await Promise.race([
471
+ dns.resolve4(domain).catch(() => dns.resolve6(domain)),
472
+ new Promise((_, reject) =>
473
+ setTimeout(() => reject(new Error('timeout')), 1000)
474
+ ),
475
+ ]);
476
+ if (addresses && addresses.length > 0) {
477
+ // Check if it's a private IP (VPC endpoint indicator)
478
+ const isPrivateIp = addresses.some(
479
+ (ip) =>
480
+ ip.startsWith('10.') ||
481
+ ip.startsWith('172.') ||
482
+ ip.startsWith('192.168.')
483
+ );
484
+ if (isPrivateIp) {
485
+ results.vpcEndpoints.push(domain);
486
+ }
487
+ }
488
+ } catch (e) {
489
+ // Expected for non-existent endpoints
490
+ }
491
+ }
492
+
493
+ results.isInVpc =
494
+ !results.hasInternetAccess || results.vpcEndpoints.length > 0;
495
+ results.canConnectToAws =
496
+ results.hasInternetAccess || results.vpcEndpoints.length > 0;
497
+ } catch (error) {
498
+ console.error('VPC detection error:', error.message);
499
+ }
500
+
501
+ return results;
502
+ };
503
+
504
+ // KMS decrypt capability check
505
+ const checkKmsDecryptCapability = async () => {
506
+ const start = Date.now();
507
+ const { KMS_KEY_ARN } = process.env;
508
+ if (!KMS_KEY_ARN) {
509
+ return {
510
+ status: 'skipped',
511
+ reason: 'KMS_KEY_ARN not configured',
512
+ };
513
+ }
514
+
515
+ // Log environment for debugging
516
+ console.log('KMS Check Debug:', {
517
+ hasKmsKeyArn: !!KMS_KEY_ARN,
518
+ kmsKeyArnPrefix: KMS_KEY_ARN?.substring(0, 30),
519
+ awsRegion: process.env.AWS_REGION,
520
+ hasDiscoveryKey: !!process.env.AWS_DISCOVERY_KMS_KEY_ID,
521
+ });
522
+
523
+ // First, detect VPC configuration
524
+ const vpcConfig = await detectVpcConfiguration();
525
+ console.log('VPC Configuration:', vpcConfig);
526
+
527
+ // Test DNS resolution for KMS endpoint
528
+ try {
529
+ const dns = require('dns').promises;
530
+ const region = process.env.AWS_REGION; // Lambda always provides this
531
+ const kmsEndpoint = `kms.${region}.amazonaws.com`;
532
+ console.log('Testing DNS resolution for:', kmsEndpoint);
533
+
534
+ // Wrap DNS resolution in a timeout
535
+ const dnsPromise = dns.resolve4(kmsEndpoint);
536
+ const timeoutPromise = new Promise((_, reject) =>
537
+ setTimeout(() => reject(new Error('DNS resolution timeout')), 3000)
538
+ );
539
+
540
+ const addresses = await Promise.race([dnsPromise, timeoutPromise]);
541
+ console.log('KMS endpoint resolved to:', addresses);
542
+
543
+ // Check if resolved to private IP (VPC endpoint)
544
+ const isVpcEndpoint = addresses.some(
545
+ (ip) =>
546
+ ip.startsWith('10.') ||
547
+ ip.startsWith('172.') ||
548
+ ip.startsWith('192.168.')
549
+ );
550
+
551
+ if (isVpcEndpoint) {
552
+ console.log(
553
+ 'KMS VPC Endpoint detected - using private connectivity'
554
+ );
555
+ }
556
+
557
+ // Test TCP connectivity to KMS (port 443)
558
+ const net = require('net');
559
+ const testConnection = () =>
560
+ new Promise((resolve) => {
561
+ const socket = new net.Socket();
562
+ const connectionTimeout = setTimeout(() => {
563
+ socket.destroy();
564
+ resolve({ connected: false, error: 'Connection timeout' });
565
+ }, 3000);
566
+
567
+ socket.on('connect', () => {
568
+ clearTimeout(connectionTimeout);
569
+ socket.destroy();
570
+ resolve({ connected: true });
571
+ });
572
+
573
+ socket.on('error', (err) => {
574
+ clearTimeout(connectionTimeout);
575
+ resolve({ connected: false, error: err.message });
576
+ });
577
+
578
+ // Try connecting to first resolved address on HTTPS port
579
+ socket.connect(443, addresses[0]);
580
+ });
581
+
582
+ const connResult = await testConnection();
583
+ console.log('TCP connectivity test:', connResult);
584
+
585
+ if (!connResult.connected) {
586
+ return {
587
+ status: 'unhealthy',
588
+ error: `Cannot connect to KMS endpoint: ${connResult.error}`,
589
+ dnsResolved: true,
590
+ tcpConnection: false,
591
+ vpcConfig,
592
+ latencyMs: Date.now() - start,
593
+ };
594
+ }
595
+ } catch (dnsError) {
596
+ console.error('DNS resolution failed:', dnsError.message);
597
+ return {
598
+ status: 'unhealthy',
599
+ error: `Cannot resolve KMS endpoint: ${dnsError.message}`,
600
+ dnsResolved: false,
601
+ vpcConfig,
602
+ latencyMs: Date.now() - start,
603
+ };
604
+ }
605
+
606
+ try {
607
+ // Use AWS SDK v3 for consistency with the rest of the codebase
608
+ // eslint-disable-next-line global-require
609
+ const {
610
+ KMSClient,
611
+ GenerateDataKeyCommand,
612
+ DecryptCommand,
613
+ } = require('@aws-sdk/client-kms');
614
+
615
+ // Lambda always provides AWS_REGION
616
+ const region = process.env.AWS_REGION;
617
+
618
+ const kms = new KMSClient({
619
+ region,
620
+ requestHandler: {
621
+ connectionTimeout: 10000, // 10 second connection timeout
622
+ requestTimeout: 25000, // 25 second timeout for slow VPC connections
623
+ },
624
+ maxAttempts: 1, // No retries on health checks
625
+ });
626
+
627
+ // Generate a data key (without plaintext logging) then immediately decrypt ciphertext to ensure decrypt perms.
628
+ const dataKeyResp = await kms.send(
629
+ new GenerateDataKeyCommand({
630
+ KeyId: KMS_KEY_ARN,
631
+ KeySpec: 'AES_256',
632
+ })
633
+ );
634
+ const decryptResp = await kms.send(
635
+ new DecryptCommand({ CiphertextBlob: dataKeyResp.CiphertextBlob })
636
+ );
637
+
638
+ const success = Boolean(
639
+ dataKeyResp.CiphertextBlob && decryptResp.Plaintext
640
+ );
641
+
642
+ return {
643
+ status: success ? 'healthy' : 'unhealthy',
644
+ kmsKeyArnSuffix: KMS_KEY_ARN.slice(-12),
645
+ vpcConfig,
646
+ latencyMs: Date.now() - start,
647
+ };
648
+ } catch (error) {
649
+ return {
650
+ status: 'unhealthy',
651
+ error: error.message,
652
+ vpcConfig,
653
+ latencyMs: Date.now() - start,
654
+ };
655
+ }
656
+ };
657
+
658
+ router.get('/health', async (_req, res) => {
659
+ const status = {
660
+ status: 'ok',
661
+ timestamp: new Date().toISOString(),
662
+ service: 'frigg-core-api',
663
+ };
664
+
665
+ res.status(200).json(status);
666
+ });
667
+
668
+ router.get('/health/detailed', async (_req, res) => {
669
+ // eslint-disable-next-line no-console
670
+ console.log('Starting detailed health check');
671
+ const startTime = Date.now();
672
+ const response = buildHealthCheckResponse(startTime);
673
+
674
+ // Log environment before any async operations
675
+ console.log('Health Check Environment:', {
676
+ hasKmsKeyArn: !!process.env.KMS_KEY_ARN,
677
+ awsRegion: process.env.AWS_REGION,
678
+ awsDefaultRegion: process.env.AWS_DEFAULT_REGION,
679
+ nodeEnv: process.env.NODE_ENV,
680
+ stage: process.env.STAGE,
681
+ });
682
+
683
+ // 1. Network diagnostics (run first to understand connectivity)
684
+ try {
685
+ console.log('Running network diagnostics...');
686
+ const networkStart = Date.now();
687
+ response.checks.network = await Promise.race([
688
+ detectVpcConfiguration(),
689
+ new Promise((_, reject) =>
690
+ setTimeout(
691
+ () => reject(new Error('Network diagnostics timeout')),
692
+ 5000
693
+ )
694
+ ),
695
+ ]);
696
+ response.checks.network.latencyMs = Date.now() - networkStart;
697
+ console.log('Network diagnostics completed:', response.checks.network);
698
+ } catch (error) {
699
+ response.checks.network = {
700
+ status: 'error',
701
+ error: error.message,
702
+ };
703
+ console.log('Network diagnostics error:', error.message);
704
+ }
705
+
706
+ // 2. KMS decrypt capability (must succeed before DB assumed healthy if encryption depends on KMS)
707
+ try {
708
+ console.log('About to check KMS capability...');
709
+ // Wrap the entire KMS check in a timeout (allow up to 25 seconds for slow VPC)
710
+ const kmsCheckPromise = checkKmsDecryptCapability();
711
+ const kmsTimeoutPromise = new Promise((_, reject) =>
712
+ setTimeout(
713
+ () => reject(new Error('KMS check timeout after 25 seconds')),
714
+ 25000
715
+ )
716
+ );
717
+
718
+ response.checks.kms = await Promise.race([
719
+ kmsCheckPromise,
720
+ kmsTimeoutPromise,
721
+ ]);
722
+ if (response.checks.kms.status === 'unhealthy') {
723
+ response.status = 'unhealthy';
724
+ }
725
+ // eslint-disable-next-line no-console
726
+ console.log('KMS check completed:', response.checks.kms);
727
+ } catch (error) {
728
+ response.checks.kms = { status: 'unhealthy', error: error.message };
729
+ response.status = 'unhealthy';
730
+ // eslint-disable-next-line no-console
731
+ console.log('KMS check error:', error.message);
732
+ }
733
+
734
+ try {
735
+ response.checks.database = await checkDatabaseHealth();
736
+ const dbState = getDatabaseState();
737
+ if (!dbState.isConnected) {
738
+ response.status = 'unhealthy';
739
+ }
740
+ // eslint-disable-next-line no-console
741
+ console.log('Database check completed:', response.checks.database);
742
+ } catch (error) {
743
+ response.checks.database = {
744
+ status: 'unhealthy',
745
+ error: error.message,
746
+ };
747
+ response.status = 'unhealthy';
748
+ // eslint-disable-next-line no-console
749
+ console.log('Database check error:', error.message);
750
+ }
751
+
752
+ try {
753
+ response.checks.encryption = await checkEncryptionHealth();
754
+ if (response.checks.encryption.status === 'unhealthy') {
755
+ response.status = 'unhealthy';
756
+ }
757
+ // eslint-disable-next-line no-console
758
+ console.log('Encryption check completed:', response.checks.encryption);
759
+ } catch (error) {
760
+ response.checks.encryption = {
761
+ status: 'unhealthy',
762
+ error: error.message,
763
+ };
764
+ response.status = 'unhealthy';
765
+ // eslint-disable-next-line no-console
766
+ console.log('Encryption check error:', error.message);
767
+ }
768
+
769
+ const { apiStatuses, allReachable } = await checkExternalAPIs();
770
+ response.checks.externalApis = apiStatuses;
771
+ if (!allReachable) {
772
+ response.status = 'unhealthy';
773
+ }
774
+ // eslint-disable-next-line no-console
775
+ console.log('External APIs check completed:', response.checks.externalApis);
776
+
777
+ try {
778
+ response.checks.integrations = checkIntegrations();
779
+ // eslint-disable-next-line no-console
780
+ console.log(
781
+ 'Integrations check completed:',
782
+ response.checks.integrations
783
+ );
784
+ } catch (error) {
785
+ response.checks.integrations = {
786
+ status: 'unhealthy',
787
+ error: error.message,
788
+ };
789
+ response.status = 'unhealthy';
790
+ // eslint-disable-next-line no-console
791
+ console.log('Integrations check error:', error.message);
792
+ }
793
+
794
+ response.responseTime = response.calculateResponseTime();
795
+ delete response.calculateResponseTime;
796
+
797
+ const statusCode = response.status === 'healthy' ? 200 : 503;
798
+ res.status(statusCode).json(response);
799
+
800
+ // eslint-disable-next-line no-console
801
+ console.log(
802
+ 'Final health status:',
803
+ response.status,
804
+ 'Response time:',
805
+ response.responseTime
806
+ );
807
+ });
808
+
809
+ router.get('/health/live', (_req, res) => {
810
+ res.status(200).json({
811
+ status: 'alive',
812
+ timestamp: new Date().toISOString(),
813
+ });
814
+ });
815
+
816
+ router.get('/health/ready', async (_req, res) => {
817
+ const dbState = getDatabaseState();
818
+ const isDbReady = dbState.isConnected;
819
+
820
+ let areModulesReady = false;
821
+ try {
822
+ const moduleTypes = Array.isArray(moduleFactory.moduleTypes)
823
+ ? moduleFactory.moduleTypes
824
+ : [];
825
+ areModulesReady = moduleTypes.length > 0;
826
+ } catch (error) {
827
+ areModulesReady = false;
828
+ }
829
+
830
+ const isReady = isDbReady && areModulesReady;
831
+
832
+ res.status(isReady ? 200 : 503).json({
833
+ ready: isReady,
834
+ timestamp: new Date().toISOString(),
835
+ checks: {
836
+ database: isDbReady,
837
+ modules: areModulesReady,
838
+ },
839
+ });
840
+ });
841
+
842
+ const handler = createAppHandler('HTTP Event: Health', router);
843
+
844
+ module.exports = { handler, router };