@friggframework/core 2.0.0-next.39 → 2.0.0-next.40
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.
- package/handlers/routers/health.js +309 -0
- package/package.json +5 -5
|
@@ -406,6 +406,255 @@ const buildHealthCheckResponse = (startTime) => {
|
|
|
406
406
|
};
|
|
407
407
|
};
|
|
408
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
|
+
|
|
409
658
|
router.get('/health', async (_req, res) => {
|
|
410
659
|
const status = {
|
|
411
660
|
status: 'ok',
|
|
@@ -422,6 +671,66 @@ router.get('/health/detailed', async (_req, res) => {
|
|
|
422
671
|
const startTime = Date.now();
|
|
423
672
|
const response = buildHealthCheckResponse(startTime);
|
|
424
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
|
+
|
|
425
734
|
try {
|
|
426
735
|
response.checks.database = await checkDatabaseHealth();
|
|
427
736
|
const dbState = getDatabaseState();
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@friggframework/core",
|
|
3
3
|
"prettier": "@friggframework/prettier-config",
|
|
4
|
-
"version": "2.0.0-next.
|
|
4
|
+
"version": "2.0.0-next.40",
|
|
5
5
|
"dependencies": {
|
|
6
6
|
"@hapi/boom": "^10.0.1",
|
|
7
7
|
"aws-sdk": "^2.1200.0",
|
|
@@ -22,9 +22,9 @@
|
|
|
22
22
|
"uuid": "^9.0.1"
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
|
-
"@friggframework/eslint-config": "2.0.0-next.
|
|
26
|
-
"@friggframework/prettier-config": "2.0.0-next.
|
|
27
|
-
"@friggframework/test": "2.0.0-next.
|
|
25
|
+
"@friggframework/eslint-config": "2.0.0-next.40",
|
|
26
|
+
"@friggframework/prettier-config": "2.0.0-next.40",
|
|
27
|
+
"@friggframework/test": "2.0.0-next.40",
|
|
28
28
|
"@types/lodash": "4.17.15",
|
|
29
29
|
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
|
30
30
|
"chai": "^4.3.6",
|
|
@@ -56,5 +56,5 @@
|
|
|
56
56
|
"publishConfig": {
|
|
57
57
|
"access": "public"
|
|
58
58
|
},
|
|
59
|
-
"gitHead": "
|
|
59
|
+
"gitHead": "088c50c6e1e37a6d42be05af49349b70ae94ee31"
|
|
60
60
|
}
|