@friggframework/devtools 2.0.0--canary.461.4116d1e.0 → 2.0.0--canary.461.77c8d12.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.
@@ -989,7 +989,7 @@ class VpcBuilder extends InfrastructureBuilder {
989
989
  }
990
990
 
991
991
  // VPC Endpoint Security Group (only if KMS, Secrets Manager, or SQS are not stack-managed and missing)
992
- const needsSecurityGroup =
992
+ const needsSecurityGroup =
993
993
  (!stackManagedEndpoints.kms && !existingEndpoints.kms && appDefinition.encryption?.fieldLevelEncryptionMethod === 'kms') ||
994
994
  (!stackManagedEndpoints.secretsManager && !existingEndpoints.secretsManager) ||
995
995
  (!stackManagedEndpoints.sqs && !existingEndpoints.sqs);
@@ -14,8 +14,10 @@
14
14
  */
15
15
 
16
16
  class CloudFormationDiscovery {
17
- constructor(provider) {
17
+ constructor(provider, config = {}) {
18
18
  this.provider = provider;
19
+ this.serviceName = config.serviceName;
20
+ this.stage = config.stage;
19
21
  }
20
22
 
21
23
  /**
@@ -41,9 +43,8 @@ class CloudFormationDiscovery {
41
43
  }
42
44
 
43
45
  // Extract from resources (now async to query AWS for details)
44
- if (resources && resources.length > 0) {
45
- await this._extractFromResources(resources, discovered);
46
- }
46
+ // Always call this even if resources is empty, as it may query AWS for resources
47
+ await this._extractFromResources(resources || [], discovered);
47
48
 
48
49
  return discovered;
49
50
  } catch (error) {
@@ -200,6 +201,30 @@ class CloudFormationDiscovery {
200
201
  }
201
202
  }
202
203
 
204
+ // KMS Key Alias - query to get the actual key ARN
205
+ if (LogicalResourceId === 'FriggKMSKeyAlias' && ResourceType === 'AWS::KMS::Alias') {
206
+ discovered.kmsKeyAlias = PhysicalResourceId;
207
+ console.log(` ✓ Found KMS key alias in stack: ${PhysicalResourceId}`);
208
+
209
+ // Query KMS to get the key ARN that this alias points to
210
+ // Always query even if key is already set, to ensure consistency
211
+ if (this.provider && this.provider.describeKmsKey) {
212
+ try {
213
+ console.log(` Querying KMS to get key ARN from alias...`);
214
+ const keyMetadata = await this.provider.describeKmsKey(PhysicalResourceId);
215
+
216
+ if (keyMetadata) {
217
+ discovered.defaultKmsKeyId = keyMetadata.Arn;
218
+ console.log(` ✓ Extracted KMS key ARN from alias: ${discovered.defaultKmsKeyId}`);
219
+ } else {
220
+ console.warn(` ⚠️ KMS key query returned no metadata`);
221
+ }
222
+ } catch (error) {
223
+ console.warn(` ⚠️ Could not get key ARN from alias: ${error.message}`);
224
+ }
225
+ }
226
+ }
227
+
203
228
  // Subnets
204
229
  if (LogicalResourceId === 'FriggPrivateSubnet1' && ResourceType === 'AWS::EC2::Subnet') {
205
230
  discovered.privateSubnetId1 = PhysicalResourceId;
@@ -243,7 +268,7 @@ class CloudFormationDiscovery {
243
268
  }
244
269
 
245
270
  // If we have a VPC ID but no subnet IDs, query EC2 for Frigg-managed subnets
246
- if (discovered.defaultVpcId && this.provider &&
271
+ if (discovered.defaultVpcId && this.provider &&
247
272
  !discovered.privateSubnetId1 && !discovered.publicSubnetId1) {
248
273
  try {
249
274
  console.log(' Querying EC2 for Frigg-managed subnets...');
@@ -266,7 +291,7 @@ class CloudFormationDiscovery {
266
291
  }));
267
292
 
268
293
  // Find private subnets
269
- const privateSubnets = subnets.filter(s => !s.isPublic).sort((a, b) =>
294
+ const privateSubnets = subnets.filter(s => !s.isPublic).sort((a, b) =>
270
295
  a.logicalId?.localeCompare(b.logicalId) || 0
271
296
  );
272
297
  if (privateSubnets.length >= 1) {
@@ -277,7 +302,7 @@ class CloudFormationDiscovery {
277
302
  }
278
303
 
279
304
  // Find public subnets
280
- const publicSubnets = subnets.filter(s => s.isPublic).sort((a, b) =>
305
+ const publicSubnets = subnets.filter(s => s.isPublic).sort((a, b) =>
281
306
  a.logicalId?.localeCompare(b.logicalId) || 0
282
307
  );
283
308
  if (publicSubnets.length >= 1) {
@@ -293,6 +318,27 @@ class CloudFormationDiscovery {
293
318
  console.warn(` ⚠️ Could not query EC2 for subnets: ${error.message}`);
294
319
  }
295
320
  }
321
+
322
+ // Check for KMS key alias via AWS API if not found in stack resources
323
+ // This handles cases where the alias was created outside CloudFormation
324
+ if (!discovered.defaultKmsKeyId && !discovered.kmsKeyAlias &&
325
+ this.provider && this.provider.describeKmsKey && this.serviceName && this.stage) {
326
+ try {
327
+ const aliasName = `alias/${this.serviceName}-${this.stage}-frigg-kms`;
328
+ console.log(` Querying KMS for alias: ${aliasName}...`);
329
+
330
+ const keyMetadata = await this.provider.describeKmsKey(aliasName);
331
+
332
+ if (keyMetadata) {
333
+ discovered.defaultKmsKeyId = keyMetadata.Arn;
334
+ discovered.kmsKeyAlias = aliasName;
335
+ console.log(` ✓ Found KMS key via alias query: ${discovered.defaultKmsKeyId}`);
336
+ }
337
+ } catch (error) {
338
+ // Alias not found - this is expected if no KMS key exists yet
339
+ console.log(` ℹ No KMS key alias found via AWS API`);
340
+ }
341
+ }
296
342
  }
297
343
  }
298
344
 
@@ -399,6 +399,143 @@ describe('CloudFormationDiscovery', () => {
399
399
  expect(result.defaultVpcId).toBe('vpc-123');
400
400
  expect(result.privateSubnetId1).toBeUndefined();
401
401
  });
402
+
403
+ it('should extract KMS key alias from stack resources and query for key ARN', async () => {
404
+ const mockStack = {
405
+ StackName: 'test-stack',
406
+ Outputs: [],
407
+ };
408
+
409
+ const mockResources = [
410
+ {
411
+ LogicalResourceId: 'FriggKMSKeyAlias',
412
+ PhysicalResourceId: 'alias/test-service-dev-frigg-kms',
413
+ ResourceType: 'AWS::KMS::Alias',
414
+ },
415
+ ];
416
+
417
+ mockProvider.describeStack.mockResolvedValue(mockStack);
418
+ mockProvider.listStackResources.mockResolvedValue(mockResources);
419
+ mockProvider.describeKmsKey = jest.fn().mockResolvedValue({
420
+ KeyId: 'abc-123',
421
+ Arn: 'arn:aws:kms:us-east-1:123456789:key/abc-123',
422
+ });
423
+
424
+ const result = await cfDiscovery.discoverFromStack('test-stack');
425
+
426
+ expect(result.defaultKmsKeyId).toBe('arn:aws:kms:us-east-1:123456789:key/abc-123');
427
+ expect(result.kmsKeyAlias).toBe('alias/test-service-dev-frigg-kms');
428
+ expect(mockProvider.describeKmsKey).toHaveBeenCalledWith('alias/test-service-dev-frigg-kms');
429
+ });
430
+
431
+ it('should query AWS API for KMS alias when serviceName and stage are provided', async () => {
432
+ const mockStack = {
433
+ StackName: 'test-stack',
434
+ Outputs: [],
435
+ };
436
+
437
+ const mockResources = [];
438
+
439
+ mockProvider.describeStack.mockResolvedValue(mockStack);
440
+ mockProvider.listStackResources.mockResolvedValue(mockResources);
441
+ mockProvider.region = 'us-east-1';
442
+ mockProvider.describeKmsKey = jest.fn().mockResolvedValue({
443
+ KeyId: 'abc-123',
444
+ Arn: 'arn:aws:kms:us-east-1:123456789:key/abc-123',
445
+ });
446
+
447
+ // Pass serviceName and stage to discover alias
448
+ cfDiscovery.serviceName = 'test-service';
449
+ cfDiscovery.stage = 'dev';
450
+
451
+ const result = await cfDiscovery.discoverFromStack('test-stack');
452
+
453
+ expect(result.defaultKmsKeyId).toBe('arn:aws:kms:us-east-1:123456789:key/abc-123');
454
+ expect(result.kmsKeyAlias).toBe('alias/test-service-dev-frigg-kms');
455
+ expect(mockProvider.describeKmsKey).toHaveBeenCalledWith('alias/test-service-dev-frigg-kms');
456
+ });
457
+
458
+ it('should handle KMS alias not found gracefully', async () => {
459
+ const mockStack = {
460
+ StackName: 'test-stack',
461
+ Outputs: [],
462
+ };
463
+
464
+ const mockResources = [];
465
+
466
+ mockProvider.describeStack.mockResolvedValue(mockStack);
467
+ mockProvider.listStackResources.mockResolvedValue(mockResources);
468
+ mockProvider.region = 'us-east-1';
469
+ mockProvider.describeKmsKey = jest.fn().mockRejectedValue(
470
+ new Error('Alias/test-service-dev-frigg-kms is not found')
471
+ );
472
+
473
+ cfDiscovery.serviceName = 'test-service';
474
+ cfDiscovery.stage = 'dev';
475
+
476
+ const result = await cfDiscovery.discoverFromStack('test-stack');
477
+
478
+ expect(result.defaultKmsKeyId).toBeUndefined();
479
+ expect(result.kmsKeyAlias).toBeUndefined();
480
+ });
481
+
482
+ it('should prefer KMS key from stack resources over alias query', async () => {
483
+ const mockStack = {
484
+ StackName: 'test-stack',
485
+ Outputs: [],
486
+ };
487
+
488
+ const mockResources = [
489
+ {
490
+ LogicalResourceId: 'FriggKMSKey',
491
+ PhysicalResourceId: 'arn:aws:kms:us-east-1:123456789:key/xyz-789',
492
+ ResourceType: 'AWS::KMS::Key',
493
+ },
494
+ ];
495
+
496
+ mockProvider.describeStack.mockResolvedValue(mockStack);
497
+ mockProvider.listStackResources.mockResolvedValue(mockResources);
498
+ mockProvider.describeKmsKey = jest.fn();
499
+
500
+ const result = await cfDiscovery.discoverFromStack('test-stack');
501
+
502
+ // Should use the key from stack resources, not query for alias
503
+ expect(result.defaultKmsKeyId).toBe('arn:aws:kms:us-east-1:123456789:key/xyz-789');
504
+ expect(mockProvider.describeKmsKey).not.toHaveBeenCalled();
505
+ });
506
+
507
+ it('should use KMS alias from stack resources even if key is also present', async () => {
508
+ const mockStack = {
509
+ StackName: 'test-stack',
510
+ Outputs: [],
511
+ };
512
+
513
+ const mockResources = [
514
+ {
515
+ LogicalResourceId: 'FriggKMSKey',
516
+ PhysicalResourceId: 'arn:aws:kms:us-east-1:123456789:key/xyz-789',
517
+ ResourceType: 'AWS::KMS::Key',
518
+ },
519
+ {
520
+ LogicalResourceId: 'FriggKMSKeyAlias',
521
+ PhysicalResourceId: 'alias/test-service-dev-frigg-kms',
522
+ ResourceType: 'AWS::KMS::Alias',
523
+ },
524
+ ];
525
+
526
+ mockProvider.describeStack.mockResolvedValue(mockStack);
527
+ mockProvider.listStackResources.mockResolvedValue(mockResources);
528
+ mockProvider.describeKmsKey = jest.fn().mockResolvedValue({
529
+ KeyId: 'xyz-789',
530
+ Arn: 'arn:aws:kms:us-east-1:123456789:key/xyz-789',
531
+ });
532
+
533
+ const result = await cfDiscovery.discoverFromStack('test-stack');
534
+
535
+ expect(result.defaultKmsKeyId).toBe('arn:aws:kms:us-east-1:123456789:key/xyz-789');
536
+ expect(result.kmsKeyAlias).toBe('alias/test-service-dev-frigg-kms');
537
+ expect(mockProvider.describeKmsKey).toHaveBeenCalledWith('alias/test-service-dev-frigg-kms');
538
+ });
402
539
  });
403
540
  });
404
541
 
@@ -469,9 +469,29 @@ class AWSProviderAdapter extends CloudProviderAdapter {
469
469
  return result;
470
470
  }
471
471
 
472
+ /**
473
+ * Describe KMS key by key ID or alias
474
+ *
475
+ * @param {string} keyIdOrAlias - Key ID or alias name
476
+ * @returns {Promise<Object>} Key metadata
477
+ */
478
+ async describeKmsKey(keyIdOrAlias) {
479
+ const kms = this.getKMSClient();
480
+
481
+ try {
482
+ const response = await kms.send(new DescribeKeyCommand({
483
+ KeyId: keyIdOrAlias,
484
+ }));
485
+
486
+ return response.KeyMetadata;
487
+ } catch (error) {
488
+ throw new Error(`Failed to describe KMS key ${keyIdOrAlias}: ${error.message}`);
489
+ }
490
+ }
491
+
472
492
  /**
473
493
  * Describe CloudFormation stack
474
- *
494
+ *
475
495
  * @param {string} stackName - Name of the CloudFormation stack
476
496
  * @returns {Promise<Object>} Stack details including outputs
477
497
  */
@@ -73,9 +73,10 @@ async function gatherDiscoveredResources(appDefinition) {
73
73
  // Build discovery configuration
74
74
  const stage = process.env.SLS_STAGE || 'dev';
75
75
  const stackName = `${appDefinition.name || 'create-frigg-app'}-${stage}`;
76
+ const serviceName = appDefinition.name || 'create-frigg-app';
76
77
 
77
78
  // Try CloudFormation-first discovery
78
- const cfDiscovery = new CloudFormationDiscovery(provider);
79
+ const cfDiscovery = new CloudFormationDiscovery(provider, { serviceName, stage });
79
80
  const stackResources = await cfDiscovery.discoverFromStack(stackName);
80
81
 
81
82
  // Validate CF discovery results - only use if contains useful data
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@friggframework/devtools",
3
3
  "prettier": "@friggframework/prettier-config",
4
- "version": "2.0.0--canary.461.4116d1e.0",
4
+ "version": "2.0.0--canary.461.77c8d12.0",
5
5
  "dependencies": {
6
6
  "@aws-sdk/client-ec2": "^3.835.0",
7
7
  "@aws-sdk/client-kms": "^3.835.0",
@@ -11,8 +11,8 @@
11
11
  "@babel/eslint-parser": "^7.18.9",
12
12
  "@babel/parser": "^7.25.3",
13
13
  "@babel/traverse": "^7.25.3",
14
- "@friggframework/schemas": "2.0.0--canary.461.4116d1e.0",
15
- "@friggframework/test": "2.0.0--canary.461.4116d1e.0",
14
+ "@friggframework/schemas": "2.0.0--canary.461.77c8d12.0",
15
+ "@friggframework/test": "2.0.0--canary.461.77c8d12.0",
16
16
  "@hapi/boom": "^10.0.1",
17
17
  "@inquirer/prompts": "^5.3.8",
18
18
  "axios": "^1.7.2",
@@ -34,8 +34,8 @@
34
34
  "serverless-http": "^2.7.0"
35
35
  },
36
36
  "devDependencies": {
37
- "@friggframework/eslint-config": "2.0.0--canary.461.4116d1e.0",
38
- "@friggframework/prettier-config": "2.0.0--canary.461.4116d1e.0",
37
+ "@friggframework/eslint-config": "2.0.0--canary.461.77c8d12.0",
38
+ "@friggframework/prettier-config": "2.0.0--canary.461.77c8d12.0",
39
39
  "aws-sdk-client-mock": "^4.1.0",
40
40
  "aws-sdk-client-mock-jest": "^4.1.0",
41
41
  "jest": "^30.1.3",
@@ -70,5 +70,5 @@
70
70
  "publishConfig": {
71
71
  "access": "public"
72
72
  },
73
- "gitHead": "4116d1e999b2ef801f8433755d107b34a12ad817"
73
+ "gitHead": "77c8d12f6a33bb2d66e19b43b6d96ee27d6f5e06"
74
74
  }