@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.
@@ -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.39",
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.39",
26
- "@friggframework/prettier-config": "2.0.0-next.39",
27
- "@friggframework/test": "2.0.0-next.39",
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": "38a7b885828dca923a6ea0d2e297d0048ba06822"
59
+ "gitHead": "088c50c6e1e37a6d42be05af49349b70ae94ee31"
60
60
  }