@friggframework/devtools 2.0.0--canary.490.1b7bbcb.0 → 2.0.0--canary.490.7464242.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.
@@ -534,6 +534,7 @@ class VpcBuilder extends InfrastructureBuilder {
534
534
  iamStatements: [],
535
535
  outputs: {},
536
536
  environment: {},
537
+ discovery: discoveredResources, // Store for backwards compatibility checks
537
538
  };
538
539
 
539
540
  // Add IAM permissions for VPC-enabled Lambda functions
@@ -552,7 +553,7 @@ class VpcBuilder extends InfrastructureBuilder {
552
553
  this.buildNatGatewayFromDecision(decisions.natGateway, appDefinition, discoveredResources, result);
553
554
 
554
555
  // Build VPC Endpoints based on ownership decisions
555
- this.buildVpcEndpointsFromDecisions(decisions.vpcEndpoints, decisions.securityGroup, appDefinition, result);
556
+ this.buildVpcEndpointsFromDecisions(decisions.vpcEndpoints, decisions.securityGroup, appDefinition, discoveredResources, result);
556
557
 
557
558
  // Set VPC_ENABLED environment variable
558
559
  result.environment.VPC_ENABLED = 'true';
@@ -920,7 +921,7 @@ class VpcBuilder extends InfrastructureBuilder {
920
921
  /**
921
922
  * Build VPC Endpoints based on ownership decisions
922
923
  */
923
- buildVpcEndpointsFromDecisions(endpointDecisions, securityGroupDecision, appDefinition, result) {
924
+ buildVpcEndpointsFromDecisions(endpointDecisions, securityGroupDecision, appDefinition, discoveredResources, result) {
924
925
  const decisions = endpointDecisions; // For backwards compatibility with existing code
925
926
  const endpointsToCreate = [];
926
927
  const endpointsInStack = [];
@@ -940,7 +941,7 @@ class VpcBuilder extends InfrastructureBuilder {
940
941
  if (endpointsInStack.length > 0) {
941
942
  console.log(` ✓ VPC Endpoints in stack: ${endpointsInStack.join(', ')}`);
942
943
  // CRITICAL: Must add stack-managed endpoints back to template or CloudFormation will DELETE them!
943
- this._addStackManagedEndpointsToTemplate(decisions, securityGroupDecision, result);
944
+ this._addStackManagedEndpointsToTemplate(decisions, securityGroupDecision, discoveredResources, result);
944
945
  }
945
946
 
946
947
  if (externalEndpoints.length > 0) {
@@ -1139,19 +1140,24 @@ class VpcBuilder extends InfrastructureBuilder {
1139
1140
  *
1140
1141
  * @private
1141
1142
  */
1142
- _addStackManagedEndpointsToTemplate(endpointDecisions, securityGroupDecision, result) {
1143
+ _addStackManagedEndpointsToTemplate(endpointDecisions, securityGroupDecision, discoveredResources, result) {
1143
1144
  const decisions = endpointDecisions; // For backwards compatibility
1144
1145
  const vpcId = result.vpcId;
1146
+
1147
+ // Determine logical IDs based on what exists in stack for backwards compatibility
1148
+ // CRITICAL: Frontify production uses OLD naming (VPCEndpointS3, not FriggS3VPCEndpoint)
1149
+ const existingLogicalIds = discoveredResources?.existingLogicalIds || [];
1150
+
1145
1151
  const logicalIdMap = {
1146
- s3: 'FriggS3VPCEndpoint',
1147
- dynamodb: 'FriggDynamoDBVPCEndpoint',
1148
- kms: 'FriggKMSVPCEndpoint',
1149
- secretsManager: 'FriggSecretsManagerVPCEndpoint',
1150
- sqs: 'FriggSQSVPCEndpoint'
1152
+ s3: existingLogicalIds.includes('VPCEndpointS3') ? 'VPCEndpointS3' : 'FriggS3VPCEndpoint',
1153
+ dynamodb: existingLogicalIds.includes('VPCEndpointDynamoDB') ? 'VPCEndpointDynamoDB' : 'FriggDynamoDBVPCEndpoint',
1154
+ kms: existingLogicalIds.includes('VPCEndpointKMS') ? 'VPCEndpointKMS' : 'FriggKMSVPCEndpoint',
1155
+ secretsManager: existingLogicalIds.includes('VPCEndpointSecretsManager') ? 'VPCEndpointSecretsManager' : 'FriggSecretsManagerVPCEndpoint',
1156
+ sqs: existingLogicalIds.includes('VPCEndpointSQS') ? 'VPCEndpointSQS' : 'FriggSQSVPCEndpoint'
1151
1157
  };
1152
1158
 
1153
1159
  Object.entries(decisions).forEach(([type, decision]) => {
1154
- if (decision.ownership === ResourceOwnership.STACK && decision.physicalId) {
1160
+ if (decision.ownership === ResourceOwnership.STACK) {
1155
1161
  const logicalId = logicalIdMap[type];
1156
1162
 
1157
1163
  // Determine endpoint type and properties based on service
@@ -1757,12 +1763,30 @@ class VpcBuilder extends InfrastructureBuilder {
1757
1763
  /**
1758
1764
  * Create route table and associations for NAT Gateway
1759
1765
  * Always adds to template - CloudFormation handles idempotency
1766
+ * Uses existing logical IDs from stack to prevent AlreadyExists errors
1760
1767
  */
1761
1768
  createNatGatewayRouting(appDefinition, discoveredResources, result, natGatewayId) {
1762
1769
  // Note: We always add routing resources to the template.
1763
1770
  // CloudFormation's idempotency ensures existing resources are updated, not recreated.
1764
1771
  // Removing resources from the template causes CloudFormation to try CREATE on next deploy → AlreadyExists error
1765
1772
 
1773
+ // Determine which logical ID to use for the NAT route based on what exists in stack
1774
+ // Older stacks use 'FriggNATRoute', newer ones use 'FriggPrivateRoute'
1775
+ // CRITICAL: Must check existingLogicalIds to avoid AlreadyExists errors on logical ID mismatch
1776
+ const existingLogicalIds = discoveredResources?.existingLogicalIds || [];
1777
+
1778
+ const routeLogicalId = existingLogicalIds.includes('FriggNATRoute')
1779
+ ? 'FriggNATRoute' // Use existing logical ID from stack (backwards compatibility)
1780
+ : 'FriggPrivateRoute'; // Default for new stacks
1781
+
1782
+ // Determine logical IDs for subnet associations (similar backwards compatibility)
1783
+ const subnet1AssocLogicalId = existingLogicalIds.includes('FriggSubnet1RouteAssociation')
1784
+ ? 'FriggSubnet1RouteAssociation' // Use existing (backwards compatibility)
1785
+ : 'FriggPrivateSubnet1RouteTableAssociation'; // Default for new stacks
1786
+ const subnet2AssocLogicalId = existingLogicalIds.includes('FriggSubnet2RouteAssociation')
1787
+ ? 'FriggSubnet2RouteAssociation' // Use existing (backwards compatibility)
1788
+ : 'FriggPrivateSubnet2RouteTableAssociation'; // Default for new stacks
1789
+
1766
1790
  // Private route table with NAT Gateway route
1767
1791
  if (!result.resources.FriggLambdaRouteTable) {
1768
1792
  result.resources.FriggLambdaRouteTable = {
@@ -1777,7 +1801,7 @@ class VpcBuilder extends InfrastructureBuilder {
1777
1801
  };
1778
1802
  }
1779
1803
 
1780
- result.resources.FriggPrivateRoute = {
1804
+ result.resources[routeLogicalId] = {
1781
1805
  Type: 'AWS::EC2::Route',
1782
1806
  Properties: {
1783
1807
  RouteTableId: { Ref: 'FriggLambdaRouteTable' },
@@ -1791,7 +1815,7 @@ class VpcBuilder extends InfrastructureBuilder {
1791
1815
  const subnet1Id = discoveredResources.privateSubnetId1 || { Ref: 'FriggPrivateSubnet1' };
1792
1816
  const subnet2Id = discoveredResources.privateSubnetId2 || { Ref: 'FriggPrivateSubnet2' };
1793
1817
 
1794
- result.resources.FriggPrivateSubnet1RouteTableAssociation = {
1818
+ result.resources[subnet1AssocLogicalId] = {
1795
1819
  Type: 'AWS::EC2::SubnetRouteTableAssociation',
1796
1820
  Properties: {
1797
1821
  SubnetId: subnet1Id,
@@ -1799,7 +1823,7 @@ class VpcBuilder extends InfrastructureBuilder {
1799
1823
  },
1800
1824
  };
1801
1825
 
1802
- result.resources.FriggPrivateSubnet2RouteTableAssociation = {
1826
+ result.resources[subnet2AssocLogicalId] = {
1803
1827
  Type: 'AWS::EC2::SubnetRouteTableAssociation',
1804
1828
  Properties: {
1805
1829
  SubnetId: subnet2Id,
@@ -1816,7 +1840,9 @@ class VpcBuilder extends InfrastructureBuilder {
1816
1840
  */
1817
1841
  ensureSubnetAssociations(appDefinition, discoveredResources, result) {
1818
1842
  // Skip if associations already created (by NAT Gateway routing)
1819
- if (result.resources.FriggPrivateSubnet1RouteTableAssociation) {
1843
+ // Check for both old and new logical ID patterns
1844
+ if (result.resources.FriggPrivateSubnet1RouteTableAssociation ||
1845
+ result.resources.FriggSubnet1RouteAssociation) {
1820
1846
  return; // Already handled by NAT Gateway routing
1821
1847
  }
1822
1848
 
@@ -1349,27 +1349,31 @@ describe('VpcBuilder', () => {
1349
1349
  });
1350
1350
 
1351
1351
  describe('External VPC with stack-managed routing infrastructure pattern', () => {
1352
- it('should correctly handle external VPC with stack-managed routing infrastructure', async () => {
1352
+ it('should correctly handle external VPC with NEW logical IDs (FriggPrivateRoute pattern)', async () => {
1353
1353
  // This pattern occurs when VPC/subnets/NAT are external but routing (route tables,
1354
1354
  // VPC endpoints, security groups) are managed by CloudFormation stack
1355
+ // This tests the NEWER naming convention
1355
1356
  const appDefinition = {
1356
1357
  vpc: { enable: true },
1357
1358
  encryption: { fieldLevelEncryptionMethod: 'kms' },
1359
+ database: {
1360
+ dynamodb: { enable: true } // Enable DynamoDB to create DynamoDB VPC endpoint
1361
+ }
1358
1362
  };
1359
1363
 
1360
- // Discovery results from real-world production scenario
1364
+ // Discovery results from real-world production scenario (newer stack)
1361
1365
  const discoveredResources = {
1362
1366
  fromCloudFormationStack: true,
1363
1367
  stackName: 'test-production-stack',
1364
1368
  existingLogicalIds: [
1365
1369
  'FriggLambdaSecurityGroup',
1366
1370
  'FriggLambdaRouteTable',
1367
- 'FriggPrivateRoute',
1368
- 'FriggPrivateSubnet1RouteTableAssociation',
1369
- 'FriggPrivateSubnet2RouteTableAssociation',
1370
- 'FriggS3VPCEndpoint',
1371
- 'FriggDynamoDBVPCEndpoint',
1372
- 'FriggKMSVPCEndpoint'
1371
+ 'FriggPrivateRoute', // NEW naming
1372
+ 'FriggPrivateSubnet1RouteTableAssociation', // NEW naming
1373
+ 'FriggPrivateSubnet2RouteTableAssociation', // NEW naming
1374
+ 'FriggS3VPCEndpoint', // NEW naming
1375
+ 'FriggDynamoDBVPCEndpoint', // NEW naming
1376
+ 'FriggKMSVPCEndpoint' // NEW naming
1373
1377
  ],
1374
1378
  // Stack resources (from CloudFormation)
1375
1379
  lambdaSecurityGroupId: 'sg-01002240c6a446202',
@@ -1446,6 +1450,91 @@ describe('VpcBuilder', () => {
1446
1450
  expect(friggResources).not.toContain('FriggPrivateSubnet1');
1447
1451
  expect(friggResources).not.toContain('FriggNATGateway');
1448
1452
  });
1453
+
1454
+ it('should use OLD logical IDs for backwards compatibility (FriggNATRoute, VPCEndpointS3 pattern)', async () => {
1455
+ // CRITICAL TEST: Real Frontify production stack uses OLD naming convention
1456
+ // Stack currently has: FriggNATRoute, VPCEndpointS3, VPCEndpointDynamoDB
1457
+ // We MUST use these same logical IDs to avoid AlreadyExists errors
1458
+ const appDefinition = {
1459
+ vpc: {
1460
+ enable: true,
1461
+ ownership: {
1462
+ securityGroup: 'external'
1463
+ },
1464
+ external: {
1465
+ securityGroupIds: ['sg-0c5e0d0e4a2f5efcf']
1466
+ }
1467
+ },
1468
+ encryption: { fieldLevelEncryptionMethod: 'kms' },
1469
+ database: {
1470
+ dynamodb: { enable: true }
1471
+ }
1472
+ };
1473
+
1474
+ // Discovery results matching ACTUAL Frontify production stack
1475
+ const discoveredResources = {
1476
+ fromCloudFormationStack: true,
1477
+ stackName: 'create-frigg-app-production',
1478
+ existingLogicalIds: [
1479
+ 'FriggLambdaRouteTable',
1480
+ 'FriggNATRoute', // OLD naming
1481
+ 'FriggSubnet1RouteAssociation', // OLD naming
1482
+ 'FriggSubnet2RouteAssociation', // OLD naming
1483
+ 'VPCEndpointS3', // OLD naming
1484
+ 'VPCEndpointDynamoDB' // OLD naming
1485
+ ],
1486
+ // Structured discovery (what resolver needs)
1487
+ _structured: {
1488
+ stackManaged: [
1489
+ { logicalId: 'FriggLambdaRouteTable', physicalId: 'rtb-08af43bbf0775602d', resourceType: 'AWS::EC2::RouteTable' },
1490
+ { logicalId: 'FriggNATRoute', physicalId: 'rtb-08af43bbf0775602d|0.0.0.0/0', resourceType: 'AWS::EC2::Route' },
1491
+ { logicalId: 'VPCEndpointS3', physicalId: 'vpce-0352ceac2124c14be', resourceType: 'AWS::EC2::VPCEndpoint' },
1492
+ { logicalId: 'VPCEndpointDynamoDB', physicalId: 'vpce-0b06c4f631199ea68', resourceType: 'AWS::EC2::VPCEndpoint' }
1493
+ ],
1494
+ external: [
1495
+ { physicalId: 'vpc-01cd124575c683a17', resourceType: 'AWS::EC2::VPC' },
1496
+ { physicalId: 'sg-0c5e0d0e4a2f5efcf', resourceType: 'AWS::EC2::SecurityGroup' },
1497
+ { physicalId: 'subnet-0bbca02e9981df72c', resourceType: 'AWS::EC2::Subnet' },
1498
+ { physicalId: 'subnet-005f7092b91efaaeb', resourceType: 'AWS::EC2::Subnet' },
1499
+ { physicalId: 'nat-05a536cbe7056325f', resourceType: 'AWS::EC2::NatGateway' }
1500
+ ]
1501
+ },
1502
+ // Flat discovery (for backwards compatibility)
1503
+ routeTableId: 'rtb-08af43bbf0775602d',
1504
+ natRoute: 'rtb-08af43bbf0775602d|0.0.0.0/0',
1505
+ s3VpcEndpointId: 'vpce-0352ceac2124c14be',
1506
+ dynamodbVpcEndpointId: 'vpce-0b06c4f631199ea68',
1507
+ // External resources
1508
+ defaultVpcId: 'vpc-01cd124575c683a17',
1509
+ defaultSecurityGroupId: 'sg-0c5e0d0e4a2f5efcf',
1510
+ privateSubnetId1: 'subnet-0bbca02e9981df72c',
1511
+ privateSubnetId2: 'subnet-005f7092b91efaaeb',
1512
+ existingNatGatewayId: 'nat-05a536cbe7056325f'
1513
+ };
1514
+
1515
+ const result = await vpcBuilder.build(appDefinition, discoveredResources);
1516
+
1517
+ // CRITICAL: Must use OLD logical IDs to match existing stack
1518
+ expect(result.resources.FriggNATRoute).toBeDefined();
1519
+ expect(result.resources.FriggNATRoute.Type).toBe('AWS::EC2::Route');
1520
+ expect(result.resources.FriggPrivateRoute).toBeUndefined(); // Should NOT create new ID
1521
+
1522
+ expect(result.resources.FriggSubnet1RouteAssociation).toBeDefined();
1523
+ expect(result.resources.FriggSubnet2RouteAssociation).toBeDefined();
1524
+ expect(result.resources.FriggPrivateSubnet1RouteTableAssociation).toBeUndefined();
1525
+ expect(result.resources.FriggPrivateSubnet2RouteTableAssociation).toBeUndefined();
1526
+
1527
+ expect(result.resources.VPCEndpointS3).toBeDefined();
1528
+ expect(result.resources.VPCEndpointS3.Type).toBe('AWS::EC2::VPCEndpoint');
1529
+ expect(result.resources.FriggS3VPCEndpoint).toBeUndefined(); // Should NOT create new ID
1530
+
1531
+ expect(result.resources.VPCEndpointDynamoDB).toBeDefined();
1532
+ expect(result.resources.VPCEndpointDynamoDB.Type).toBe('AWS::EC2::VPCEndpoint');
1533
+ expect(result.resources.FriggDynamoDBVPCEndpoint).toBeUndefined(); // Should NOT create new ID
1534
+
1535
+ // Route table should still be created
1536
+ expect(result.resources.FriggLambdaRouteTable).toBeDefined();
1537
+ });
1449
1538
  });
1450
1539
 
1451
1540
  describe('convertFlatDiscoveryToStructured - Direct Properties', () => {
@@ -411,9 +411,20 @@ class VpcResourceResolver extends BaseResourceResolver {
411
411
 
412
412
  /**
413
413
  * Resolve individual VPC endpoint
414
+ * Checks both old and new logical ID patterns for backwards compatibility
414
415
  * @private
415
416
  */
416
417
  _resolveEndpoint(logicalId, endpointType, userIntent, appDefinition, discovery) {
418
+ // Map of old logical IDs (for backwards compatibility with stacks created before naming standardization)
419
+ const oldLogicalIdMap = {
420
+ 'FriggS3VPCEndpoint': 'VPCEndpointS3',
421
+ 'FriggDynamoDBVPCEndpoint': 'VPCEndpointDynamoDB',
422
+ 'FriggKMSVPCEndpoint': 'VPCEndpointKMS',
423
+ 'FriggSecretsManagerVPCEndpoint': 'VPCEndpointSecretsManager',
424
+ 'FriggSQSVPCEndpoint': 'VPCEndpointSQS'
425
+ };
426
+ const oldLogicalId = oldLogicalIdMap[logicalId];
427
+
417
428
  // Explicit external
418
429
  if (userIntent === 'external') {
419
430
  const externalId = appDefinition.vpc?.external?.vpcEndpointIds?.[endpointType];
@@ -427,22 +438,35 @@ class VpcResourceResolver extends BaseResourceResolver {
427
438
  return { ownership: null, reason: `External ${endpointType} endpoint ID not provided` };
428
439
  }
429
440
 
430
- // Explicit stack
441
+ // Explicit stack - check both old and new logical IDs
431
442
  if (userIntent === 'stack') {
432
- const inStack = this.findInStack(logicalId, discovery);
443
+ let inStack = this.findInStack(logicalId, discovery);
444
+ if (!inStack && oldLogicalId) {
445
+ inStack = this.findInStack(oldLogicalId, discovery);
446
+ }
433
447
  return this.createStackDecision(
434
448
  inStack?.physicalId,
435
449
  `User specified ownership=stack for ${endpointType} endpoint`
436
450
  );
437
451
  }
438
452
 
439
- // Auto-decide - check if in stack first for more informative logging
440
- const inStack = this.isInStack(logicalId, discovery);
441
- const stackResource = inStack ? this.findInStack(logicalId, discovery) : null;
453
+ // Auto-decide - check if in stack first (try both old and new logical IDs)
454
+ let inStack = this.isInStack(logicalId, discovery);
455
+ let stackResource = inStack ? this.findInStack(logicalId, discovery) : null;
456
+ let actualLogicalId = logicalId; // Track which ID we found
457
+
458
+ // If not found with new ID, try old ID pattern
459
+ if (!inStack && oldLogicalId) {
460
+ inStack = this.isInStack(oldLogicalId, discovery);
461
+ stackResource = inStack ? this.findInStack(oldLogicalId, discovery) : null;
462
+ if (inStack) {
463
+ actualLogicalId = oldLogicalId; // Found with old ID - use that for resolution
464
+ }
465
+ }
442
466
 
443
467
  const decision = this.resolveResourceOwnership(
444
468
  'auto',
445
- logicalId,
469
+ actualLogicalId, // Use the actual ID we found (old or new)
446
470
  'AWS::EC2::VPCEndpoint',
447
471
  discovery
448
472
  );
@@ -219,7 +219,10 @@ class CloudFormationDiscovery {
219
219
  const { LogicalResourceId, PhysicalResourceId, ResourceType } = resource;
220
220
 
221
221
  // Track Frigg-managed resources by logical ID
222
- if (LogicalResourceId.startsWith('Frigg') || LogicalResourceId.includes('Migration')) {
222
+ // Include VPC endpoints with legacy naming (VPCEndpointS3, VPCEndpointDynamoDB, etc.)
223
+ if (LogicalResourceId.startsWith('Frigg') ||
224
+ LogicalResourceId.includes('Migration') ||
225
+ LogicalResourceId.startsWith('VPCEndpoint')) {
223
226
  discovered.existingLogicalIds.push(LogicalResourceId);
224
227
  }
225
228
 
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.1b7bbcb.0",
4
+ "version": "2.0.0--canary.490.7464242.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.1b7bbcb.0",
20
- "@friggframework/schemas": "2.0.0--canary.490.1b7bbcb.0",
21
- "@friggframework/test": "2.0.0--canary.490.1b7bbcb.0",
19
+ "@friggframework/core": "2.0.0--canary.490.7464242.0",
20
+ "@friggframework/schemas": "2.0.0--canary.490.7464242.0",
21
+ "@friggframework/test": "2.0.0--canary.490.7464242.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.1b7bbcb.0",
50
- "@friggframework/prettier-config": "2.0.0--canary.490.1b7bbcb.0",
49
+ "@friggframework/eslint-config": "2.0.0--canary.490.7464242.0",
50
+ "@friggframework/prettier-config": "2.0.0--canary.490.7464242.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": "1b7bbcb96b98d3a5a6cb2fb0fcfa06fb5235433e"
82
+ "gitHead": "74642426e7abdfded0cf0f124e18864341b11b1f"
83
83
  }