@friggframework/core 2.0.0--canary.427.972de91.0 → 2.0.0--canary.428.42e0806.0

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.
@@ -248,53 +248,9 @@ const testEncryption = async () => {
248
248
  };
249
249
 
250
250
  const testDoc = new TestModel(testData);
251
-
252
- try {
253
- // eslint-disable-next-line no-console
254
- console.log('Attempting to save document with encryption...');
255
- const startTime = Date.now();
256
-
257
- await withTimeout(
258
- testDoc.save(),
259
- 30000,
260
- 'Save operation timed out after 30 seconds'
261
- );
262
-
263
- const duration = Date.now() - startTime;
264
- // eslint-disable-next-line no-console
265
- console.log(`Test document saved successfully in ${duration}ms`);
266
- } catch (error) {
267
- // eslint-disable-next-line no-console
268
- console.error('Save operation failed:', {
269
- errorName: error.name,
270
- errorMessage: error.message,
271
- errorStack: error.stack,
272
- mongooseConnectionState: testDoc.db.readyState,
273
- modelName: TestModel.modelName,
274
- });
275
-
276
- // Try to get more details about the connection
277
- if (mongoose.connection && mongoose.connection.db) {
278
- try {
279
- const admin = mongoose.connection.db.admin();
280
- const serverStatus = await admin.serverStatus();
281
- // eslint-disable-next-line no-console
282
- console.log('DocumentDB Server Status:', {
283
- version: serverStatus.version,
284
- uptime: serverStatus.uptime,
285
- connections: serverStatus.connections,
286
- });
287
- } catch (statusError) {
288
- // eslint-disable-next-line no-console
289
- console.error(
290
- 'Could not get server status:',
291
- statusError.message
292
- );
293
- }
294
- }
295
-
296
- throw error;
297
- }
251
+ await withTimeout(testDoc.save(), 5000, 'Save operation timed out');
252
+ // eslint-disable-next-line no-console
253
+ console.log('Test document saved');
298
254
 
299
255
  try {
300
256
  const retrievedDoc = await withTimeout(
@@ -333,193 +289,6 @@ const testEncryption = async () => {
333
289
  }
334
290
  };
335
291
 
336
- const checkKMSAccess = async () => {
337
- const { KMS_KEY_ARN } = process.env;
338
-
339
- if (!KMS_KEY_ARN || KMS_KEY_ARN.trim() === '') {
340
- return {
341
- status: 'disabled',
342
- testResult: 'No KMS key configured',
343
- canAccessKey: false,
344
- };
345
- }
346
-
347
- try {
348
- // eslint-disable-next-line no-console
349
- console.log(
350
- 'Testing KMS key access with key:',
351
- KMS_KEY_ARN.substring(0, 50) + '...'
352
- );
353
-
354
- const AWS = require('aws-sdk');
355
- const kms = new AWS.KMS();
356
-
357
- // First, check if the master key exists and is accessible
358
- let keyExists = false;
359
- let keyMetadata = null;
360
-
361
- try {
362
- // eslint-disable-next-line no-console
363
- console.log('Checking if KMS master key exists...');
364
- const describeResult = await withTimeout(
365
- kms.describeKey({ KeyId: KMS_KEY_ARN }).promise(),
366
- 5000,
367
- 'KMS describeKey operation timed out after 5 seconds'
368
- );
369
-
370
- keyMetadata = describeResult.KeyMetadata;
371
- keyExists = true;
372
- // eslint-disable-next-line no-console
373
- console.log('KMS master key found:', {
374
- KeyId: keyMetadata.KeyId,
375
- KeyState: keyMetadata.KeyState,
376
- Enabled: keyMetadata.Enabled,
377
- KeyUsage: keyMetadata.KeyUsage,
378
- });
379
- } catch (describeError) {
380
- // eslint-disable-next-line no-console
381
- console.error(
382
- 'KMS master key does not exist or is not accessible:',
383
- describeError.message
384
- );
385
-
386
- if (describeError.code === 'NotFoundException') {
387
- return {
388
- status: 'unhealthy',
389
- testResult: 'KMS master key not found',
390
- canAccessKey: false,
391
- error: 'Master key does not exist',
392
- keyExists: false,
393
- };
394
- } else if (describeError.code === 'AccessDeniedException') {
395
- return {
396
- status: 'unhealthy',
397
- testResult: 'No permission to access KMS master key',
398
- canAccessKey: false,
399
- error: 'Access denied to master key',
400
- keyExists: 'unknown',
401
- };
402
- }
403
- // Continue to try generateDataKey even if describeKey fails
404
- // as permissions might be limited
405
- }
406
-
407
- // Check if key is in a usable state
408
- if (keyExists && keyMetadata) {
409
- // Only 'Enabled' state allows cryptographic operations
410
- if (keyMetadata.KeyState !== 'Enabled') {
411
- // eslint-disable-next-line no-console
412
- console.error(
413
- `KMS master key exists but is in state: ${keyMetadata.KeyState}`
414
- );
415
-
416
- let testResult = '';
417
- switch (keyMetadata.KeyState) {
418
- case 'Disabled':
419
- testResult = 'KMS master key is disabled';
420
- break;
421
- case 'PendingDeletion':
422
- testResult = 'KMS master key is pending deletion';
423
- break;
424
- case 'PendingImport':
425
- testResult = 'KMS master key is pending import';
426
- break;
427
- case 'Unavailable':
428
- testResult =
429
- 'KMS master key is unavailable (custom key store disconnected)';
430
- break;
431
- case 'Creating':
432
- testResult = 'KMS master key is still being created';
433
- break;
434
- case 'Updating':
435
- testResult = 'KMS master key is being updated';
436
- break;
437
- default:
438
- testResult = `KMS master key is in unusable state: ${keyMetadata.KeyState}`;
439
- }
440
-
441
- return {
442
- status: 'unhealthy',
443
- testResult,
444
- canAccessKey: false,
445
- keyExists: true,
446
- keyState: keyMetadata.KeyState,
447
- };
448
- }
449
- }
450
-
451
- // Try to generate a data key to test full KMS access
452
- // eslint-disable-next-line no-console
453
- console.log('Attempting to generate data key...');
454
- const startTime = Date.now();
455
- const result = await withTimeout(
456
- kms
457
- .generateDataKey({
458
- KeyId: KMS_KEY_ARN,
459
- KeySpec: 'AES_256',
460
- })
461
- .promise(),
462
- 10000,
463
- 'KMS generateDataKey operation timed out after 10 seconds'
464
- );
465
-
466
- const responseTime = Date.now() - startTime;
467
-
468
- // If we got a result with plaintext key, KMS access works
469
- if (result && result.Plaintext) {
470
- // eslint-disable-next-line no-console
471
- console.log(
472
- `KMS key access successful, response time: ${responseTime}ms`
473
- );
474
- return {
475
- status: 'healthy',
476
- testResult:
477
- 'Successfully requested and received decrypt key from KMS',
478
- canAccessKey: true,
479
- responseTime,
480
- keyExists: true,
481
- keyState: keyMetadata?.KeyState || 'Enabled',
482
- };
483
- }
484
-
485
- return {
486
- status: 'unhealthy',
487
- testResult: 'KMS responded but no key data received',
488
- canAccessKey: false,
489
- responseTime,
490
- keyExists: keyExists,
491
- };
492
- } catch (error) {
493
- // eslint-disable-next-line no-console
494
- console.error('KMS generateDataKey failed:', error.message);
495
-
496
- // Provide more specific error messages based on error codes
497
- let testResult = `KMS access failed: ${error.message}`;
498
- if (error.code === 'NotFoundException') {
499
- testResult = 'KMS master key not found during data key generation';
500
- // eslint-disable-next-line no-console
501
- console.error('Master key does not exist in KMS');
502
- } else if (error.code === 'AccessDeniedException') {
503
- testResult =
504
- 'Access denied - check IAM permissions for kms:GenerateDataKey';
505
- // eslint-disable-next-line no-console
506
- console.error('IAM permissions insufficient for KMS operations');
507
- } else if (error.code === 'InvalidKeyId.NotFound') {
508
- testResult = 'Invalid KMS key ID format or key not found';
509
- // eslint-disable-next-line no-console
510
- console.error('KMS key ID is invalid or does not exist');
511
- }
512
-
513
- return {
514
- status: 'unhealthy',
515
- testResult,
516
- canAccessKey: false,
517
- error: error.message,
518
- errorCode: error.code,
519
- };
520
- }
521
- };
522
-
523
292
  const checkEncryptionHealth = async () => {
524
293
  const config = getEncryptionConfiguration();
525
294
 
@@ -637,6 +406,131 @@ const buildHealthCheckResponse = (startTime) => {
637
406
  };
638
407
  };
639
408
 
409
+ // KMS decrypt capability check
410
+ const checkKmsDecryptCapability = async () => {
411
+ const start = Date.now();
412
+ const { KMS_KEY_ARN } = process.env;
413
+ if (!KMS_KEY_ARN) {
414
+ return {
415
+ status: 'skipped',
416
+ reason: 'KMS_KEY_ARN not configured',
417
+ };
418
+ }
419
+
420
+ // Log environment for debugging
421
+ console.log('KMS Check Debug:', {
422
+ hasKmsKeyArn: !!KMS_KEY_ARN,
423
+ kmsKeyArnPrefix: KMS_KEY_ARN?.substring(0, 30),
424
+ awsRegion: process.env.AWS_REGION,
425
+ awsDefaultRegion: process.env.AWS_DEFAULT_REGION,
426
+ hasDiscoveryKey: !!process.env.AWS_DISCOVERY_KMS_KEY_ID,
427
+ });
428
+
429
+ // Test DNS resolution for KMS endpoint
430
+ try {
431
+ const dns = require('dns').promises;
432
+ const region = process.env.AWS_REGION || process.env.AWS_DEFAULT_REGION || 'eu-central-1';
433
+ const kmsEndpoint = `kms.${region}.amazonaws.com`;
434
+ console.log('Testing DNS resolution for:', kmsEndpoint);
435
+ const addresses = await dns.resolve4(kmsEndpoint);
436
+ console.log('KMS endpoint resolved to:', addresses);
437
+
438
+ // Test TCP connectivity to KMS (port 443)
439
+ const net = require('net');
440
+ const testConnection = () => new Promise((resolve) => {
441
+ const socket = new net.Socket();
442
+ const connectionTimeout = setTimeout(() => {
443
+ socket.destroy();
444
+ resolve({ connected: false, error: 'Connection timeout' });
445
+ }, 3000);
446
+
447
+ socket.on('connect', () => {
448
+ clearTimeout(connectionTimeout);
449
+ socket.destroy();
450
+ resolve({ connected: true });
451
+ });
452
+
453
+ socket.on('error', (err) => {
454
+ clearTimeout(connectionTimeout);
455
+ resolve({ connected: false, error: err.message });
456
+ });
457
+
458
+ // Try connecting to first resolved address on HTTPS port
459
+ socket.connect(443, addresses[0]);
460
+ });
461
+
462
+ const connResult = await testConnection();
463
+ console.log('TCP connectivity test:', connResult);
464
+
465
+ if (!connResult.connected) {
466
+ return {
467
+ status: 'unhealthy',
468
+ error: `Cannot connect to KMS endpoint: ${connResult.error}`,
469
+ dnsResolved: true,
470
+ tcpConnection: false,
471
+ latencyMs: Date.now() - start,
472
+ };
473
+ }
474
+ } catch (dnsError) {
475
+ console.error('DNS resolution failed:', dnsError.message);
476
+ return {
477
+ status: 'unhealthy',
478
+ error: `Cannot resolve KMS endpoint: ${dnsError.message}`,
479
+ dnsResolved: false,
480
+ latencyMs: Date.now() - start,
481
+ };
482
+ }
483
+
484
+ try {
485
+ // Lazy load to avoid cost if unused in some environments
486
+ // eslint-disable-next-line global-require
487
+ const AWS = require('aws-sdk');
488
+
489
+ // Ensure AWS SDK has proper configuration
490
+ const region = process.env.AWS_REGION || process.env.AWS_DEFAULT_REGION;
491
+ if (!region) {
492
+ return {
493
+ status: 'unhealthy',
494
+ error: 'AWS_REGION not configured',
495
+ latencyMs: Date.now() - start,
496
+ };
497
+ }
498
+
499
+ const kms = new AWS.KMS({
500
+ region,
501
+ httpOptions: {
502
+ timeout: 5000, // 5 second timeout
503
+ connectTimeout: 5000,
504
+ },
505
+ maxRetries: 1, // Don't retry on health checks
506
+ });
507
+
508
+ // Generate a data key (without plaintext logging) then immediately decrypt ciphertext to ensure decrypt perms.
509
+ const dataKeyResp = await kms
510
+ .generateDataKey({ KeyId: KMS_KEY_ARN, KeySpec: 'AES_256' })
511
+ .promise();
512
+ const decryptResp = await kms
513
+ .decrypt({ CiphertextBlob: dataKeyResp.CiphertextBlob })
514
+ .promise();
515
+
516
+ const success = Boolean(
517
+ dataKeyResp.CiphertextBlob && decryptResp.Plaintext
518
+ );
519
+
520
+ return {
521
+ status: success ? 'healthy' : 'unhealthy',
522
+ kmsKeyArnSuffix: KMS_KEY_ARN.slice(-12),
523
+ latencyMs: Date.now() - start,
524
+ };
525
+ } catch (error) {
526
+ return {
527
+ status: 'unhealthy',
528
+ error: error.message,
529
+ latencyMs: Date.now() - start,
530
+ };
531
+ }
532
+ };
533
+
640
534
  router.get('/health', async (_req, res) => {
641
535
  const status = {
642
536
  status: 'ok',
@@ -653,113 +547,79 @@ router.get('/health/detailed', async (_req, res) => {
653
547
  const startTime = Date.now();
654
548
  const response = buildHealthCheckResponse(startTime);
655
549
 
656
- // Run all health checks in parallel for faster response
657
- // eslint-disable-next-line no-console
658
- console.log('Running all health checks in parallel...');
659
-
660
- const [
661
- kmsResult,
662
- databaseResult,
663
- encryptionResult,
664
- externalApisResult,
665
- integrationsResult,
666
- ] = await Promise.allSettled([
667
- checkKMSAccess(),
668
- checkDatabaseHealth(),
669
- checkEncryptionHealth(),
670
- checkExternalAPIs(),
671
- Promise.resolve(checkIntegrations()), // Wrap sync function in Promise
672
- ]);
673
-
674
- // Process KMS check result
675
- if (kmsResult.status === 'fulfilled') {
676
- response.checks.kmsAccess = kmsResult.value;
677
- if (kmsResult.value.status === 'unhealthy') {
550
+ // 1. KMS decrypt capability (must succeed before DB assumed healthy if encryption depends on KMS)
551
+ try {
552
+ response.checks.kms = await checkKmsDecryptCapability();
553
+ if (response.checks.kms.status === 'unhealthy') {
678
554
  response.status = 'unhealthy';
679
555
  }
680
556
  // eslint-disable-next-line no-console
681
- console.log('KMS access check completed:', response.checks.kmsAccess);
682
- } else {
683
- response.checks.kmsAccess = {
684
- status: 'unhealthy',
685
- error: kmsResult.reason?.message || 'KMS check failed',
686
- };
557
+ console.log('KMS check completed:', response.checks.kms);
558
+ } catch (error) {
559
+ response.checks.kms = { status: 'unhealthy', error: error.message };
687
560
  response.status = 'unhealthy';
688
561
  // eslint-disable-next-line no-console
689
- console.log('KMS access check error:', kmsResult.reason?.message);
562
+ console.log('KMS check error:', error.message);
690
563
  }
691
564
 
692
- // Process database check result
693
- if (databaseResult.status === 'fulfilled') {
694
- response.checks.database = databaseResult.value;
565
+ try {
566
+ response.checks.database = await checkDatabaseHealth();
695
567
  const dbState = getDatabaseState();
696
568
  if (!dbState.isConnected) {
697
569
  response.status = 'unhealthy';
698
570
  }
699
571
  // eslint-disable-next-line no-console
700
572
  console.log('Database check completed:', response.checks.database);
701
- } else {
573
+ } catch (error) {
702
574
  response.checks.database = {
703
575
  status: 'unhealthy',
704
- error: databaseResult.reason?.message || 'Database check failed',
576
+ error: error.message,
705
577
  };
706
578
  response.status = 'unhealthy';
707
579
  // eslint-disable-next-line no-console
708
- console.log('Database check error:', databaseResult.reason?.message);
580
+ console.log('Database check error:', error.message);
709
581
  }
710
582
 
711
- // Process encryption check result
712
- if (encryptionResult.status === 'fulfilled') {
713
- response.checks.encryption = encryptionResult.value;
714
- if (encryptionResult.value.status === 'unhealthy') {
583
+ try {
584
+ response.checks.encryption = await checkEncryptionHealth();
585
+ if (response.checks.encryption.status === 'unhealthy') {
715
586
  response.status = 'unhealthy';
716
587
  }
717
588
  // eslint-disable-next-line no-console
718
589
  console.log('Encryption check completed:', response.checks.encryption);
719
- } else {
590
+ } catch (error) {
720
591
  response.checks.encryption = {
721
592
  status: 'unhealthy',
722
- error: encryptionResult.reason?.message || 'Encryption check failed',
593
+ error: error.message,
723
594
  };
724
595
  response.status = 'unhealthy';
725
596
  // eslint-disable-next-line no-console
726
- console.log('Encryption check error:', encryptionResult.reason?.message);
597
+ console.log('Encryption check error:', error.message);
727
598
  }
728
599
 
729
- // Process external APIs check result
730
- if (externalApisResult.status === 'fulfilled') {
731
- const { apiStatuses, allReachable } = externalApisResult.value;
732
- response.checks.externalApis = apiStatuses;
733
- if (!allReachable) {
734
- response.status = 'unhealthy';
735
- }
736
- // eslint-disable-next-line no-console
737
- console.log('External APIs check completed:', response.checks.externalApis);
738
- } else {
739
- response.checks.externalApis = {
740
- error: externalApisResult.reason?.message || 'External APIs check failed',
741
- };
600
+ const { apiStatuses, allReachable } = await checkExternalAPIs();
601
+ response.checks.externalApis = apiStatuses;
602
+ if (!allReachable) {
742
603
  response.status = 'unhealthy';
743
- // eslint-disable-next-line no-console
744
- console.log('External APIs check error:', externalApisResult.reason?.message);
745
604
  }
605
+ // eslint-disable-next-line no-console
606
+ console.log('External APIs check completed:', response.checks.externalApis);
746
607
 
747
- // Process integrations check result
748
- if (integrationsResult.status === 'fulfilled') {
749
- response.checks.integrations = integrationsResult.value;
608
+ try {
609
+ response.checks.integrations = checkIntegrations();
750
610
  // eslint-disable-next-line no-console
751
611
  console.log(
752
612
  'Integrations check completed:',
753
613
  response.checks.integrations
754
614
  );
755
- } else {
615
+ } catch (error) {
756
616
  response.checks.integrations = {
757
617
  status: 'unhealthy',
758
- error: integrationsResult.reason?.message || 'Integrations check failed',
618
+ error: error.message,
759
619
  };
760
620
  response.status = 'unhealthy';
761
621
  // eslint-disable-next-line no-console
762
- console.log('Integrations check error:', integrationsResult.reason?.message);
622
+ console.log('Integrations check error:', error.message);
763
623
  }
764
624
 
765
625
  response.responseTime = response.calculateResponseTime();
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--canary.427.972de91.0",
4
+ "version": "2.0.0--canary.428.42e0806.0",
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--canary.427.972de91.0",
26
- "@friggframework/prettier-config": "2.0.0--canary.427.972de91.0",
27
- "@friggframework/test": "2.0.0--canary.427.972de91.0",
25
+ "@friggframework/eslint-config": "2.0.0--canary.428.42e0806.0",
26
+ "@friggframework/prettier-config": "2.0.0--canary.428.42e0806.0",
27
+ "@friggframework/test": "2.0.0--canary.428.42e0806.0",
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": "972de918c8273da9f1ffb85e1eedfc088f880f54"
59
+ "gitHead": "42e0806db6ef878a5397b3e1752ae6cf21e6a845"
60
60
  }