@friggframework/devtools 2.0.0--canary.490.71c435d.0 → 2.0.0--canary.490.36b3031.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.
@@ -577,11 +577,10 @@ class VpcBuilder extends InfrastructureBuilder {
577
577
  * Build VPC based on ownership decision
578
578
  *
579
579
  * For STACK ownership: ALWAYS add definitions to template.
580
- * CloudFormation idempotency ensures existing resources aren't recreated.
581
580
  */
582
581
  buildVpcFromDecision(decision, appDefinition, result) {
583
582
  if (decision.ownership === ResourceOwnership.STACK) {
584
- // For STACK ownership: ALWAYS create definitions (CloudFormation idempotency)
583
+ // For STACK ownership: ALWAYS create definitions
585
584
  if (decision.physicalId) {
586
585
  console.log(` → Adding VPC definition to template (existing: ${decision.physicalId})`);
587
586
  } else {
@@ -640,7 +639,6 @@ class VpcBuilder extends InfrastructureBuilder {
640
639
  buildSecurityGroupFromDecision(decision, appDefinition, result) {
641
640
  if (decision.ownership === ResourceOwnership.STACK) {
642
641
  // Always create security group resource in template
643
- // CloudFormation handles idempotency if it already exists
644
642
  console.log(' → Adding Lambda Security Group to template...');
645
643
 
646
644
  result.resources.FriggLambdaSecurityGroup = {
@@ -690,7 +688,6 @@ class VpcBuilder extends InfrastructureBuilder {
690
688
  }
691
689
 
692
690
  // For STACK ownership: ALWAYS add definitions to template
693
- // CloudFormation idempotency ensures existing resources won't be recreated
694
691
  if (decision.physicalIds && decision.physicalIds.length >= 2) {
695
692
  console.log(` → Adding subnet definitions to template (existing: ${decision.physicalIds.join(', ')})`);
696
693
  } else {
@@ -398,6 +398,66 @@ describe('VpcBuilder', () => {
398
398
  expect(result.resources.FriggS3VPCEndpoint.Properties.VpcId).toBe('vpc-123');
399
399
  });
400
400
 
401
+ it('should add stack-managed security group back to template to prevent deletion', async () => {
402
+ const appDefinition = {
403
+ vpc: { enable: true },
404
+ };
405
+
406
+ const discoveredResources = {
407
+ fromCloudFormationStack: true,
408
+ stackName: 'test-stack',
409
+ existingLogicalIds: ['FriggLambdaSecurityGroup'],
410
+ defaultVpcId: 'vpc-123',
411
+ privateSubnetId1: 'subnet-1',
412
+ privateSubnetId2: 'subnet-2',
413
+ lambdaSecurityGroupId: 'sg-existing-stack', // Existing in stack
414
+ };
415
+
416
+ const result = await vpcBuilder.build(appDefinition, discoveredResources);
417
+
418
+ // CRITICAL: Must RE-ADD stack-managed SG to template or CloudFormation will DELETE it
419
+ expect(result.resources.FriggLambdaSecurityGroup).toBeDefined();
420
+ expect(result.resources.FriggLambdaSecurityGroup.Type).toBe('AWS::EC2::SecurityGroup');
421
+ expect(result.resources.FriggLambdaSecurityGroup.Properties.VpcId).toBe('vpc-123');
422
+ expect(result.resources.FriggLambdaSecurityGroup.Properties.GroupDescription).toBeDefined();
423
+
424
+ // Should use Ref in Lambda config (not recreating)
425
+ expect(result.vpcConfig.securityGroupIds).toContainEqual({ Ref: 'FriggLambdaSecurityGroup' });
426
+ });
427
+
428
+ it('should add stack-managed subnets back to template to prevent deletion', async () => {
429
+ const appDefinition = {
430
+ vpc: { enable: true },
431
+ };
432
+
433
+ const discoveredResources = {
434
+ fromCloudFormationStack: true,
435
+ stackName: 'test-stack',
436
+ existingLogicalIds: ['FriggPrivateSubnet1', 'FriggPrivateSubnet2'],
437
+ defaultVpcId: 'vpc-123',
438
+ // Subnets exist in stack with specific IDs
439
+ privateSubnetId1: 'subnet-existing-1',
440
+ privateSubnetId2: 'subnet-existing-2',
441
+ };
442
+
443
+ const result = await vpcBuilder.build(appDefinition, discoveredResources);
444
+
445
+ // CRITICAL: Must RE-ADD stack-managed subnets to template or CloudFormation will DELETE them
446
+ expect(result.resources.FriggPrivateSubnet1).toBeDefined();
447
+ expect(result.resources.FriggPrivateSubnet1.Type).toBe('AWS::EC2::Subnet');
448
+ expect(result.resources.FriggPrivateSubnet1.Properties.VpcId).toBe('vpc-123');
449
+
450
+ expect(result.resources.FriggPrivateSubnet2).toBeDefined();
451
+ expect(result.resources.FriggPrivateSubnet2.Type).toBe('AWS::EC2::Subnet');
452
+ expect(result.resources.FriggPrivateSubnet2.Properties.VpcId).toBe('vpc-123');
453
+
454
+ // Should use Refs (not external IDs)
455
+ expect(result.vpcConfig.subnetIds).toEqual([
456
+ { Ref: 'FriggPrivateSubnet1' },
457
+ { Ref: 'FriggPrivateSubnet2' }
458
+ ]);
459
+ });
460
+
401
461
  it('should add stack-managed VPC endpoints back to template to prevent deletion', async () => {
402
462
  const appDefinition = {
403
463
  vpc: { enable: true },
@@ -1288,6 +1348,105 @@ describe('VpcBuilder', () => {
1288
1348
  });
1289
1349
  });
1290
1350
 
1351
+ describe('Full Frontify deployment pattern integration test', () => {
1352
+ it('should correctly handle Frontify production scenario: external VPC + stack routing', async () => {
1353
+ // This test replicates the EXACT Frontify production deployment scenario
1354
+ const appDefinition = {
1355
+ vpc: { enable: true },
1356
+ encryption: { fieldLevelEncryptionMethod: 'kms' },
1357
+ };
1358
+
1359
+ // Actual Frontify discovery results (from production logs)
1360
+ const discoveredResources = {
1361
+ fromCloudFormationStack: true,
1362
+ stackName: 'create-frigg-app-production',
1363
+ existingLogicalIds: [
1364
+ 'FriggLambdaSecurityGroup',
1365
+ 'FriggLambdaRouteTable',
1366
+ 'FriggPrivateRoute',
1367
+ 'FriggPrivateSubnet1RouteTableAssociation',
1368
+ 'FriggPrivateSubnet2RouteTableAssociation',
1369
+ 'FriggS3VPCEndpoint',
1370
+ 'FriggDynamoDBVPCEndpoint',
1371
+ 'FriggKMSVPCEndpoint'
1372
+ ],
1373
+ // Stack resources (from CloudFormation)
1374
+ lambdaSecurityGroupId: 'sg-01002240c6a446202',
1375
+ routeTableId: 'rtb-08af43bbf0775602d',
1376
+ s3VpcEndpointId: 'vpce-0d1ecb2c53ce9b4b8',
1377
+ dynamodbVpcEndpointId: 'vpce-0fb749b207f1020b0',
1378
+ kmsVpcEndpointId: 'vpce-0e38c25155b86de22',
1379
+ // External resources (discovered via queries)
1380
+ defaultVpcId: 'vpc-0cd17c0e06cb28b28',
1381
+ privateSubnetId1: 'subnet-034f6562dbbc16348',
1382
+ privateSubnetId2: 'subnet-0b8be2b82aeb5cdec',
1383
+ existingNatGatewayId: 'nat-022660c36a47e2d79'
1384
+ };
1385
+
1386
+ const result = await vpcBuilder.build(appDefinition, discoveredResources);
1387
+
1388
+ // === ASSERTIONS: Template Structure ===
1389
+
1390
+ // 1. VPC should be external (not in template)
1391
+ expect(result.resources.FriggVPC).toBeUndefined();
1392
+ expect(result.vpcId).toBe('vpc-0cd17c0e06cb28b28');
1393
+
1394
+ // 2. Security Group MUST be in template (stack-managed)
1395
+ expect(result.resources.FriggLambdaSecurityGroup).toBeDefined();
1396
+ expect(result.resources.FriggLambdaSecurityGroup.Type).toBe('AWS::EC2::SecurityGroup');
1397
+ expect(result.vpcConfig.securityGroupIds).toEqual([{ Ref: 'FriggLambdaSecurityGroup' }]);
1398
+
1399
+ // 3. Subnets should be external (use hardcoded IDs, not in template)
1400
+ expect(result.resources.FriggPrivateSubnet1).toBeUndefined();
1401
+ expect(result.resources.FriggPrivateSubnet2).toBeUndefined();
1402
+ expect(result.vpcConfig.subnetIds).toEqual([
1403
+ 'subnet-034f6562dbbc16348',
1404
+ 'subnet-0b8be2b82aeb5cdec'
1405
+ ]);
1406
+
1407
+ // 4. NAT Gateway should be external (not in template)
1408
+ expect(result.resources.FriggNATGateway).toBeUndefined();
1409
+ expect(result.resources.FriggNATGatewayEIP).toBeUndefined();
1410
+ expect(result.natGatewayId).toBe('nat-022660c36a47e2d79');
1411
+
1412
+ // 5. Route table MUST be in template (stack-managed)
1413
+ expect(result.resources.FriggLambdaRouteTable).toBeDefined();
1414
+ expect(result.resources.FriggLambdaRouteTable.Type).toBe('AWS::EC2::RouteTable');
1415
+
1416
+ // 6. Route table associations MUST be in template
1417
+ expect(result.resources.FriggPrivateSubnet1RouteTableAssociation).toBeDefined();
1418
+ expect(result.resources.FriggPrivateSubnet2RouteTableAssociation).toBeDefined();
1419
+
1420
+ // 7. VPC Endpoints MUST be in template (stack-managed, prevents deletion)
1421
+ expect(result.resources.FriggS3VPCEndpoint).toBeDefined();
1422
+ expect(result.resources.FriggS3VPCEndpoint.Properties.VpcEndpointType).toBe('Gateway');
1423
+
1424
+ expect(result.resources.FriggDynamoDBVPCEndpoint).toBeDefined();
1425
+ expect(result.resources.FriggDynamoDBVPCEndpoint.Properties.VpcEndpointType).toBe('Gateway');
1426
+
1427
+ expect(result.resources.FriggKMSVPCEndpoint).toBeDefined();
1428
+ expect(result.resources.FriggKMSVPCEndpoint.Properties.VpcEndpointType).toBe('Interface');
1429
+
1430
+ // 8. VPC Endpoint Security Group needed for interface endpoints
1431
+ expect(result.resources.FriggVPCEndpointSecurityGroup).toBeDefined();
1432
+
1433
+ // === ASSERTIONS: Resource Count ===
1434
+ const resourceKeys = Object.keys(result.resources);
1435
+ const friggResources = resourceKeys.filter(k => k.startsWith('Frigg') || k.startsWith('VPC'));
1436
+
1437
+ // Should have routing infrastructure + endpoints + security groups
1438
+ // NOT full VPC (no FriggVPC, FriggPrivateSubnet1/2, FriggNATGateway)
1439
+ expect(friggResources).toContain('FriggLambdaSecurityGroup');
1440
+ expect(friggResources).toContain('FriggLambdaRouteTable');
1441
+ expect(friggResources).toContain('FriggS3VPCEndpoint');
1442
+ expect(friggResources).toContain('FriggDynamoDBVPCEndpoint');
1443
+ expect(friggResources).toContain('FriggKMSVPCEndpoint');
1444
+ expect(friggResources).not.toContain('FriggVPC');
1445
+ expect(friggResources).not.toContain('FriggPrivateSubnet1');
1446
+ expect(friggResources).not.toContain('FriggNATGateway');
1447
+ });
1448
+ });
1449
+
1291
1450
  describe('convertFlatDiscoveryToStructured - CloudFormation query results', () => {
1292
1451
  it('should add VPC from CloudFormation query to external array', () => {
1293
1452
  const flatDiscovery = {
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.490.71c435d.0",
4
+ "version": "2.0.0--canary.490.36b3031.0",
5
5
  "bin": {
6
6
  "frigg": "./frigg-cli/index.js"
7
7
  },
@@ -16,9 +16,9 @@
16
16
  "@babel/eslint-parser": "^7.18.9",
17
17
  "@babel/parser": "^7.25.3",
18
18
  "@babel/traverse": "^7.25.3",
19
- "@friggframework/core": "2.0.0--canary.490.71c435d.0",
20
- "@friggframework/schemas": "2.0.0--canary.490.71c435d.0",
21
- "@friggframework/test": "2.0.0--canary.490.71c435d.0",
19
+ "@friggframework/core": "2.0.0--canary.490.36b3031.0",
20
+ "@friggframework/schemas": "2.0.0--canary.490.36b3031.0",
21
+ "@friggframework/test": "2.0.0--canary.490.36b3031.0",
22
22
  "@hapi/boom": "^10.0.1",
23
23
  "@inquirer/prompts": "^5.3.8",
24
24
  "axios": "^1.7.2",
@@ -46,8 +46,8 @@
46
46
  "validate-npm-package-name": "^5.0.0"
47
47
  },
48
48
  "devDependencies": {
49
- "@friggframework/eslint-config": "2.0.0--canary.490.71c435d.0",
50
- "@friggframework/prettier-config": "2.0.0--canary.490.71c435d.0",
49
+ "@friggframework/eslint-config": "2.0.0--canary.490.36b3031.0",
50
+ "@friggframework/prettier-config": "2.0.0--canary.490.36b3031.0",
51
51
  "aws-sdk-client-mock": "^4.1.0",
52
52
  "aws-sdk-client-mock-jest": "^4.1.0",
53
53
  "jest": "^30.1.3",
@@ -79,5 +79,5 @@
79
79
  "publishConfig": {
80
80
  "access": "public"
81
81
  },
82
- "gitHead": "71c435df870654de6b8f2594674ac35c32c2bd93"
82
+ "gitHead": "36b30311e062560a65a75a864b880bff01a745e7"
83
83
  }