@friggframework/devtools 2.0.0--canary.461.4116d1e.0 → 2.0.0--canary.461.39e4094.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) {
@@ -121,18 +122,21 @@ class CloudFormationDiscovery {
121
122
  if (LogicalResourceId === 'FriggLambdaSecurityGroup' && ResourceType === 'AWS::EC2::SecurityGroup') {
122
123
  discovered.securityGroupId = PhysicalResourceId;
123
124
  console.log(` ✓ Found security group in stack: ${PhysicalResourceId}`);
124
- // Query security group to get VPC ID
125
- if (this.provider && !discovered.defaultVpcId) {
125
+
126
+ // Query security group to get VPC ID (required because SG resource doesn't include VPC ID)
127
+ if (this.provider && this.provider.getEC2Client && !discovered.defaultVpcId) {
126
128
  try {
127
- console.log(` Querying security group to get VPC ID...`);
129
+ console.log(` Querying EC2 to get VPC ID from security group...`);
128
130
  const { DescribeSecurityGroupsCommand } = require('@aws-sdk/client-ec2');
129
- const sgDetails = await this.provider.getEC2Client().send(
131
+ const ec2Client = this.provider.getEC2Client();
132
+ const sgDetails = await ec2Client.send(
130
133
  new DescribeSecurityGroupsCommand({
131
134
  GroupIds: [PhysicalResourceId]
132
135
  })
133
136
  );
137
+
134
138
  if (sgDetails.SecurityGroups && sgDetails.SecurityGroups.length > 0) {
135
- discovered.defaultVpcId = sgDetails.SecurityGroups[0].VpcId; // VpcBuilder expects 'defaultVpcId'
139
+ discovered.defaultVpcId = sgDetails.SecurityGroups[0].VpcId;
136
140
  console.log(` ✓ Extracted VPC ID from security group: ${discovered.defaultVpcId}`);
137
141
  } else {
138
142
  console.warn(` ⚠️ Security group query returned no results`);
@@ -192,6 +196,12 @@ class CloudFormationDiscovery {
192
196
  discovered.natGatewayId = PhysicalResourceId;
193
197
  }
194
198
 
199
+ // VPC - direct extraction (primary method)
200
+ if (LogicalResourceId === 'FriggVPC' && ResourceType === 'AWS::EC2::VPC') {
201
+ discovered.defaultVpcId = PhysicalResourceId;
202
+ console.log(` ✓ Found VPC in stack: ${PhysicalResourceId}`);
203
+ }
204
+
195
205
  // KMS Key (alternative to output)
196
206
  if (LogicalResourceId === 'FriggKMSKey' && ResourceType === 'AWS::KMS::Key') {
197
207
  // Note: For KMS, we prefer the ARN from outputs, but this is a fallback
@@ -200,6 +210,30 @@ class CloudFormationDiscovery {
200
210
  }
201
211
  }
202
212
 
213
+ // KMS Key Alias - query to get the actual key ARN
214
+ if (LogicalResourceId === 'FriggKMSKeyAlias' && ResourceType === 'AWS::KMS::Alias') {
215
+ discovered.kmsKeyAlias = PhysicalResourceId;
216
+ console.log(` ✓ Found KMS key alias in stack: ${PhysicalResourceId}`);
217
+
218
+ // Query KMS to get the key ARN that this alias points to
219
+ // Always query even if key is already set, to ensure consistency
220
+ if (this.provider && this.provider.describeKmsKey) {
221
+ try {
222
+ console.log(` Querying KMS to get key ARN from alias...`);
223
+ const keyMetadata = await this.provider.describeKmsKey(PhysicalResourceId);
224
+
225
+ if (keyMetadata) {
226
+ discovered.defaultKmsKeyId = keyMetadata.Arn;
227
+ console.log(` ✓ Extracted KMS key ARN from alias: ${discovered.defaultKmsKeyId}`);
228
+ } else {
229
+ console.warn(` ⚠️ KMS key query returned no metadata`);
230
+ }
231
+ } catch (error) {
232
+ console.warn(` ⚠️ Could not get key ARN from alias: ${error.message}`);
233
+ }
234
+ }
235
+ }
236
+
203
237
  // Subnets
204
238
  if (LogicalResourceId === 'FriggPrivateSubnet1' && ResourceType === 'AWS::EC2::Subnet') {
205
239
  discovered.privateSubnetId1 = PhysicalResourceId;
@@ -243,7 +277,7 @@ class CloudFormationDiscovery {
243
277
  }
244
278
 
245
279
  // If we have a VPC ID but no subnet IDs, query EC2 for Frigg-managed subnets
246
- if (discovered.defaultVpcId && this.provider &&
280
+ if (discovered.defaultVpcId && this.provider &&
247
281
  !discovered.privateSubnetId1 && !discovered.publicSubnetId1) {
248
282
  try {
249
283
  console.log(' Querying EC2 for Frigg-managed subnets...');
@@ -266,7 +300,7 @@ class CloudFormationDiscovery {
266
300
  }));
267
301
 
268
302
  // Find private subnets
269
- const privateSubnets = subnets.filter(s => !s.isPublic).sort((a, b) =>
303
+ const privateSubnets = subnets.filter(s => !s.isPublic).sort((a, b) =>
270
304
  a.logicalId?.localeCompare(b.logicalId) || 0
271
305
  );
272
306
  if (privateSubnets.length >= 1) {
@@ -277,7 +311,7 @@ class CloudFormationDiscovery {
277
311
  }
278
312
 
279
313
  // Find public subnets
280
- const publicSubnets = subnets.filter(s => s.isPublic).sort((a, b) =>
314
+ const publicSubnets = subnets.filter(s => s.isPublic).sort((a, b) =>
281
315
  a.logicalId?.localeCompare(b.logicalId) || 0
282
316
  );
283
317
  if (publicSubnets.length >= 1) {
@@ -293,6 +327,27 @@ class CloudFormationDiscovery {
293
327
  console.warn(` ⚠️ Could not query EC2 for subnets: ${error.message}`);
294
328
  }
295
329
  }
330
+
331
+ // Check for KMS key alias via AWS API if not found in stack resources
332
+ // This handles cases where the alias was created outside CloudFormation
333
+ if (!discovered.defaultKmsKeyId && !discovered.kmsKeyAlias &&
334
+ this.provider && this.provider.describeKmsKey && this.serviceName && this.stage) {
335
+ try {
336
+ const aliasName = `alias/${this.serviceName}-${this.stage}-frigg-kms`;
337
+ console.log(` Querying KMS for alias: ${aliasName}...`);
338
+
339
+ const keyMetadata = await this.provider.describeKmsKey(aliasName);
340
+
341
+ if (keyMetadata) {
342
+ discovered.defaultKmsKeyId = keyMetadata.Arn;
343
+ discovered.kmsKeyAlias = aliasName;
344
+ console.log(` ✓ Found KMS key via alias query: ${discovered.defaultKmsKeyId}`);
345
+ }
346
+ } catch (error) {
347
+ // Alias not found - this is expected if no KMS key exists yet
348
+ console.log(` ℹ No KMS key alias found via AWS API`);
349
+ }
350
+ }
296
351
  }
297
352
  }
298
353
 
@@ -253,6 +253,30 @@ describe('CloudFormationDiscovery', () => {
253
253
  });
254
254
  });
255
255
 
256
+ it('should extract VPC directly from stack resources', async () => {
257
+ const mockStack = {
258
+ StackName: 'test-stack',
259
+ Outputs: [],
260
+ };
261
+
262
+ const mockResources = [
263
+ {
264
+ LogicalResourceId: 'FriggVPC',
265
+ PhysicalResourceId: 'vpc-037ec55fe87aec1e7',
266
+ ResourceType: 'AWS::EC2::VPC',
267
+ },
268
+ ];
269
+
270
+ mockProvider.describeStack.mockResolvedValue(mockStack);
271
+ mockProvider.listStackResources.mockResolvedValue(mockResources);
272
+
273
+ const result = await cfDiscovery.discoverFromStack('test-stack');
274
+
275
+ expect(result).toEqual({
276
+ defaultVpcId: 'vpc-037ec55fe87aec1e7',
277
+ });
278
+ });
279
+
256
280
  it('should combine outputs and resources correctly', async () => {
257
281
  const mockStack = {
258
282
  StackName: 'test-stack',
@@ -399,6 +423,143 @@ describe('CloudFormationDiscovery', () => {
399
423
  expect(result.defaultVpcId).toBe('vpc-123');
400
424
  expect(result.privateSubnetId1).toBeUndefined();
401
425
  });
426
+
427
+ it('should extract KMS key alias from stack resources and query for key ARN', async () => {
428
+ const mockStack = {
429
+ StackName: 'test-stack',
430
+ Outputs: [],
431
+ };
432
+
433
+ const mockResources = [
434
+ {
435
+ LogicalResourceId: 'FriggKMSKeyAlias',
436
+ PhysicalResourceId: 'alias/test-service-dev-frigg-kms',
437
+ ResourceType: 'AWS::KMS::Alias',
438
+ },
439
+ ];
440
+
441
+ mockProvider.describeStack.mockResolvedValue(mockStack);
442
+ mockProvider.listStackResources.mockResolvedValue(mockResources);
443
+ mockProvider.describeKmsKey = jest.fn().mockResolvedValue({
444
+ KeyId: 'abc-123',
445
+ Arn: 'arn:aws:kms:us-east-1:123456789:key/abc-123',
446
+ });
447
+
448
+ const result = await cfDiscovery.discoverFromStack('test-stack');
449
+
450
+ expect(result.defaultKmsKeyId).toBe('arn:aws:kms:us-east-1:123456789:key/abc-123');
451
+ expect(result.kmsKeyAlias).toBe('alias/test-service-dev-frigg-kms');
452
+ expect(mockProvider.describeKmsKey).toHaveBeenCalledWith('alias/test-service-dev-frigg-kms');
453
+ });
454
+
455
+ it('should query AWS API for KMS alias when serviceName and stage are provided', async () => {
456
+ const mockStack = {
457
+ StackName: 'test-stack',
458
+ Outputs: [],
459
+ };
460
+
461
+ const mockResources = [];
462
+
463
+ mockProvider.describeStack.mockResolvedValue(mockStack);
464
+ mockProvider.listStackResources.mockResolvedValue(mockResources);
465
+ mockProvider.region = 'us-east-1';
466
+ mockProvider.describeKmsKey = jest.fn().mockResolvedValue({
467
+ KeyId: 'abc-123',
468
+ Arn: 'arn:aws:kms:us-east-1:123456789:key/abc-123',
469
+ });
470
+
471
+ // Pass serviceName and stage to discover alias
472
+ cfDiscovery.serviceName = 'test-service';
473
+ cfDiscovery.stage = 'dev';
474
+
475
+ const result = await cfDiscovery.discoverFromStack('test-stack');
476
+
477
+ expect(result.defaultKmsKeyId).toBe('arn:aws:kms:us-east-1:123456789:key/abc-123');
478
+ expect(result.kmsKeyAlias).toBe('alias/test-service-dev-frigg-kms');
479
+ expect(mockProvider.describeKmsKey).toHaveBeenCalledWith('alias/test-service-dev-frigg-kms');
480
+ });
481
+
482
+ it('should handle KMS alias not found gracefully', async () => {
483
+ const mockStack = {
484
+ StackName: 'test-stack',
485
+ Outputs: [],
486
+ };
487
+
488
+ const mockResources = [];
489
+
490
+ mockProvider.describeStack.mockResolvedValue(mockStack);
491
+ mockProvider.listStackResources.mockResolvedValue(mockResources);
492
+ mockProvider.region = 'us-east-1';
493
+ mockProvider.describeKmsKey = jest.fn().mockRejectedValue(
494
+ new Error('Alias/test-service-dev-frigg-kms is not found')
495
+ );
496
+
497
+ cfDiscovery.serviceName = 'test-service';
498
+ cfDiscovery.stage = 'dev';
499
+
500
+ const result = await cfDiscovery.discoverFromStack('test-stack');
501
+
502
+ expect(result.defaultKmsKeyId).toBeUndefined();
503
+ expect(result.kmsKeyAlias).toBeUndefined();
504
+ });
505
+
506
+ it('should prefer KMS key from stack resources over alias query', async () => {
507
+ const mockStack = {
508
+ StackName: 'test-stack',
509
+ Outputs: [],
510
+ };
511
+
512
+ const mockResources = [
513
+ {
514
+ LogicalResourceId: 'FriggKMSKey',
515
+ PhysicalResourceId: 'arn:aws:kms:us-east-1:123456789:key/xyz-789',
516
+ ResourceType: 'AWS::KMS::Key',
517
+ },
518
+ ];
519
+
520
+ mockProvider.describeStack.mockResolvedValue(mockStack);
521
+ mockProvider.listStackResources.mockResolvedValue(mockResources);
522
+ mockProvider.describeKmsKey = jest.fn();
523
+
524
+ const result = await cfDiscovery.discoverFromStack('test-stack');
525
+
526
+ // Should use the key from stack resources, not query for alias
527
+ expect(result.defaultKmsKeyId).toBe('arn:aws:kms:us-east-1:123456789:key/xyz-789');
528
+ expect(mockProvider.describeKmsKey).not.toHaveBeenCalled();
529
+ });
530
+
531
+ it('should use KMS alias from stack resources even if key is also present', async () => {
532
+ const mockStack = {
533
+ StackName: 'test-stack',
534
+ Outputs: [],
535
+ };
536
+
537
+ const mockResources = [
538
+ {
539
+ LogicalResourceId: 'FriggKMSKey',
540
+ PhysicalResourceId: 'arn:aws:kms:us-east-1:123456789:key/xyz-789',
541
+ ResourceType: 'AWS::KMS::Key',
542
+ },
543
+ {
544
+ LogicalResourceId: 'FriggKMSKeyAlias',
545
+ PhysicalResourceId: 'alias/test-service-dev-frigg-kms',
546
+ ResourceType: 'AWS::KMS::Alias',
547
+ },
548
+ ];
549
+
550
+ mockProvider.describeStack.mockResolvedValue(mockStack);
551
+ mockProvider.listStackResources.mockResolvedValue(mockResources);
552
+ mockProvider.describeKmsKey = jest.fn().mockResolvedValue({
553
+ KeyId: 'xyz-789',
554
+ Arn: 'arn:aws:kms:us-east-1:123456789:key/xyz-789',
555
+ });
556
+
557
+ const result = await cfDiscovery.discoverFromStack('test-stack');
558
+
559
+ expect(result.defaultKmsKeyId).toBe('arn:aws:kms:us-east-1:123456789:key/xyz-789');
560
+ expect(result.kmsKeyAlias).toBe('alias/test-service-dev-frigg-kms');
561
+ expect(mockProvider.describeKmsKey).toHaveBeenCalledWith('alias/test-service-dev-frigg-kms');
562
+ });
402
563
  });
403
564
  });
404
565
 
@@ -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
@@ -84,19 +85,26 @@ async function gatherDiscoveredResources(appDefinition) {
84
85
  const hasAuroraData = stackResources?.auroraClusterId;
85
86
  const hasSomeUsefulData = hasVpcData || hasKmsData || hasAuroraData;
86
87
 
88
+ // Check if we're in isolated mode (each stage gets its own VPC/Aurora)
89
+ const isIsolatedMode = appDefinition.managementMode === 'managed' &&
90
+ appDefinition.vpcIsolation === 'isolated';
91
+
87
92
  if (stackResources && hasSomeUsefulData) {
88
93
  console.log(' ✓ Discovered resources from existing CloudFormation stack');
89
94
  console.log('✅ Cloud resource discovery completed successfully!');
90
95
  return stackResources;
91
96
  }
92
97
 
93
- // In isolated mode, ONLY use CloudFormation discovery for VPC/Aurora
94
- // But still discover KMS (encryption keys can be safely shared across stages)
95
- if (appDefinition.managementMode === 'managed' && appDefinition.vpcIsolation === 'isolated') {
96
- console.log(' ℹ Isolated mode: discovering KMS (shareable) but not VPC/Aurora (isolated)');
98
+ // In isolated mode, NEVER fall back to AWS discovery for VPC/Aurora
99
+ // These resources must be isolated per stage, so we either:
100
+ // 1. Use resources from THIS stage's CloudFormation stack (handled above)
101
+ // 2. Return empty to CREATE fresh isolated resources for this stage
102
+ if (isIsolatedMode) {
103
+ console.log(' ℹ Isolated mode: No CloudFormation stack or no VPC/Aurora in stack');
104
+ console.log(' ℹ Will create fresh isolated VPC/Aurora for this stage');
105
+ console.log(' ℹ Checking for shared KMS key...');
97
106
 
98
- // Still run KMS discovery - encryption keys are safe to share
99
- // Pass serviceName and stage to search for stage-specific alias
107
+ // KMS keys CAN be shared across stages (encryption keys are safe to reuse)
100
108
  const kmsDiscovery = new KmsDiscovery(provider);
101
109
  const kmsConfig = {
102
110
  serviceName: appDefinition.name || 'create-frigg-app',
@@ -107,12 +115,12 @@ async function gatherDiscoveredResources(appDefinition) {
107
115
 
108
116
  if (kmsResult?.defaultKmsKeyId) {
109
117
  console.log(' ✓ Found shared KMS key (can be reused across stages)');
110
- console.log('✅ Cloud resource discovery completed successfully!');
118
+ console.log('✅ Cloud resource discovery completed - will create isolated VPC/Aurora!');
111
119
  return kmsResult;
112
120
  }
113
121
 
114
122
  console.log(' ℹ No existing KMS key found - will create new one');
115
- console.log('✅ Cloud resource discovery completed successfully!');
123
+ console.log('✅ Cloud resource discovery completed - will create fresh isolated resources!');
116
124
  return {};
117
125
  }
118
126
 
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.39e4094.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.39e4094.0",
15
+ "@friggframework/test": "2.0.0--canary.461.39e4094.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.39e4094.0",
38
+ "@friggframework/prettier-config": "2.0.0--canary.461.39e4094.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": "39e40947472c29e3847fa53dad03ab1354568e56"
74
74
  }