@friggframework/devtools 2.0.0--canary.454.a254930.0 → 2.0.0--canary.454.726ab78.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.
@@ -12,7 +12,8 @@ const shouldRunDiscovery = (AppDefinition) => {
12
12
  return (
13
13
  AppDefinition.vpc?.enable === true ||
14
14
  AppDefinition.encryption?.fieldLevelEncryptionMethod === 'kms' ||
15
- AppDefinition.ssm?.enable === true
15
+ AppDefinition.ssm?.enable === true ||
16
+ AppDefinition.database?.postgres?.enable === true
16
17
  );
17
18
  };
18
19
 
@@ -435,10 +436,17 @@ const gatherDiscoveredResources = async (AppDefinition) => {
435
436
  try {
436
437
  const region = process.env.AWS_REGION || 'us-east-1';
437
438
  const discovery = new AWSDiscovery(region);
439
+ // Use Serverless Framework's stage resolution (opt:stage with 'dev' as default)
440
+ // This matches how serverless.yml resolves ${opt:stage, "dev"}
441
+ // IMPORTANT: Use SLS_STAGE (not STAGE) to match actual deployment stage
442
+ const stage = process.env.SLS_STAGE || 'dev';
443
+
438
444
  const config = {
439
445
  vpc: AppDefinition.vpc || {},
440
446
  encryption: AppDefinition.encryption || {},
441
447
  ssm: AppDefinition.ssm || {},
448
+ serviceName: AppDefinition.name || 'create-frigg-app',
449
+ stage: stage,
442
450
  };
443
451
 
444
452
  const discoveredResources = await discovery.discoverResources(config);
@@ -489,6 +497,22 @@ const buildEnvironment = (appEnvironmentVars, discoveredResources) => {
489
497
  }
490
498
  }
491
499
 
500
+ // Add Aurora discovery mappings
501
+ if (discoveredResources.aurora) {
502
+ if (discoveredResources.aurora.clusterIdentifier) {
503
+ environment.AWS_DISCOVERY_AURORA_CLUSTER_ID = discoveredResources.aurora.clusterIdentifier;
504
+ }
505
+ if (discoveredResources.aurora.endpoint) {
506
+ environment.AWS_DISCOVERY_AURORA_ENDPOINT = discoveredResources.aurora.endpoint;
507
+ }
508
+ if (discoveredResources.aurora.port) {
509
+ environment.AWS_DISCOVERY_AURORA_PORT = discoveredResources.aurora.port.toString();
510
+ }
511
+ if (discoveredResources.aurora.secretArn) {
512
+ environment.AWS_DISCOVERY_AURORA_SECRET_ARN = discoveredResources.aurora.secretArn;
513
+ }
514
+ }
515
+
492
516
  return environment;
493
517
  };
494
518
 
@@ -505,6 +529,7 @@ const createBaseDefinition = (AppDefinition, appEnvironmentVars, discoveredResou
505
529
  useDotenv: true,
506
530
  provider: {
507
531
  name: AppDefinition.provider || 'aws',
532
+ ...(process.env.AWS_PROFILE && { profile: process.env.AWS_PROFILE }),
508
533
  runtime: 'nodejs20.x',
509
534
  timeout: 30,
510
535
  region,
@@ -666,14 +691,21 @@ const applyKmsConfiguration = (definition, AppDefinition, discoveredResources) =
666
691
 
667
692
  if (discoveredResources.defaultKmsKeyId) {
668
693
  console.log(`Using existing KMS key: ${discoveredResources.defaultKmsKeyId}`);
669
- definition.resources.Resources.FriggKMSKeyAlias = {
670
- Type: 'AWS::KMS::Alias',
671
- DeletionPolicy: 'Retain',
672
- Properties: {
673
- AliasName: 'alias/${self:service}-${self:provider.stage}-frigg-kms',
674
- TargetKeyId: discoveredResources.defaultKmsKeyId,
675
- },
676
- };
694
+
695
+ // Only create alias if it doesn't already exist
696
+ if (!discoveredResources.kmsAliasExists) {
697
+ console.log('Creating KMS alias for discovered key...');
698
+ definition.resources.Resources.FriggKMSKeyAlias = {
699
+ Type: 'AWS::KMS::Alias',
700
+ DeletionPolicy: 'Retain',
701
+ Properties: {
702
+ AliasName: 'alias/${self:service}-${self:provider.stage}-frigg-kms',
703
+ TargetKeyId: discoveredResources.defaultKmsKeyId,
704
+ },
705
+ };
706
+ } else {
707
+ console.log('KMS alias already exists, skipping alias creation');
708
+ }
677
709
 
678
710
  definition.provider.iamRoleStatements.push({
679
711
  Effect: 'Allow',
@@ -1510,7 +1542,9 @@ const configureVpc = (definition, AppDefinition, discoveredResources) => {
1510
1542
  },
1511
1543
  };
1512
1544
 
1513
- if (AppDefinition.secretsManager?.enable === true) {
1545
+ // Create Secrets Manager VPC Endpoint if explicitly enabled OR if Aurora is enabled
1546
+ // (Aurora requires Secrets Manager access for credential retrieval)
1547
+ if (AppDefinition.secretsManager?.enable === true || AppDefinition.database?.postgres?.enable === true) {
1514
1548
  definition.resources.Resources.VPCEndpointSecretsManager = {
1515
1549
  Type: 'AWS::EC2::VPCEndpoint',
1516
1550
  Properties: {
@@ -1527,6 +1561,283 @@ const configureVpc = (definition, AppDefinition, discoveredResources) => {
1527
1561
  }
1528
1562
  };
1529
1563
 
1564
+ const createAuroraInfrastructure = (definition, AppDefinition, discoveredResources) => {
1565
+ const dbConfig = AppDefinition.database.postgres;
1566
+
1567
+ console.log('šŸ”§ Creating Aurora Serverless v2 infrastructure...');
1568
+
1569
+ // 1. DB Subnet Group (using Lambda private subnets)
1570
+ definition.resources.Resources.FriggDBSubnetGroup = {
1571
+ Type: 'AWS::RDS::DBSubnetGroup',
1572
+ Properties: {
1573
+ DBSubnetGroupDescription: 'Subnet group for Frigg Aurora cluster',
1574
+ SubnetIds: [
1575
+ discoveredResources.privateSubnetId1,
1576
+ discoveredResources.privateSubnetId2
1577
+ ],
1578
+ Tags: [
1579
+ { Key: 'Name', Value: '${self:service}-${self:provider.stage}-db-subnet-group' },
1580
+ { Key: 'ManagedBy', Value: 'Frigg' },
1581
+ { Key: 'Service', Value: '${self:service}' },
1582
+ { Key: 'Stage', Value: '${self:provider.stage}' },
1583
+ ]
1584
+ }
1585
+ };
1586
+
1587
+ // 2. Security Group (allow Lambda SG to access 5432)
1588
+ // In create-new VPC mode, Lambda uses FriggLambdaSecurityGroup
1589
+ // In other modes, use discovered default security group
1590
+ const lambdaSecurityGroupId = AppDefinition.vpc?.management === 'create-new'
1591
+ ? { Ref: 'FriggLambdaSecurityGroup' }
1592
+ : discoveredResources.defaultSecurityGroupId;
1593
+
1594
+ definition.resources.Resources.FriggAuroraSecurityGroup = {
1595
+ Type: 'AWS::EC2::SecurityGroup',
1596
+ Properties: {
1597
+ GroupDescription: 'Security group for Frigg Aurora PostgreSQL',
1598
+ VpcId: discoveredResources.defaultVpcId,
1599
+ SecurityGroupIngress: [
1600
+ {
1601
+ IpProtocol: 'tcp',
1602
+ FromPort: 5432,
1603
+ ToPort: 5432,
1604
+ SourceSecurityGroupId: lambdaSecurityGroupId,
1605
+ Description: 'PostgreSQL access from Lambda functions'
1606
+ }
1607
+ ],
1608
+ Tags: [
1609
+ { Key: 'Name', Value: '${self:service}-${self:provider.stage}-aurora-sg' },
1610
+ { Key: 'ManagedBy', Value: 'Frigg' },
1611
+ { Key: 'Service', Value: '${self:service}' },
1612
+ { Key: 'Stage', Value: '${self:provider.stage}' },
1613
+ ]
1614
+ }
1615
+ };
1616
+
1617
+ // 3. Secrets Manager Secret (database credentials)
1618
+ definition.resources.Resources.FriggDatabaseSecret = {
1619
+ Type: 'AWS::SecretsManager::Secret',
1620
+ Properties: {
1621
+ Name: '${self:service}-${self:provider.stage}-aurora-credentials',
1622
+ Description: 'Aurora PostgreSQL credentials for Frigg application',
1623
+ GenerateSecretString: {
1624
+ SecretStringTemplate: JSON.stringify({
1625
+ username: dbConfig.masterUsername || 'frigg_admin'
1626
+ }),
1627
+ GenerateStringKey: 'password',
1628
+ PasswordLength: 32,
1629
+ ExcludeCharacters: '"@/\\'
1630
+ },
1631
+ Tags: [
1632
+ { Key: 'ManagedBy', Value: 'Frigg' },
1633
+ { Key: 'Service', Value: '${self:service}' },
1634
+ { Key: 'Stage', Value: '${self:provider.stage}' },
1635
+ ]
1636
+ }
1637
+ };
1638
+
1639
+ // 4. Aurora Serverless v2 Cluster
1640
+ definition.resources.Resources.FriggAuroraCluster = {
1641
+ Type: 'AWS::RDS::DBCluster',
1642
+ DeletionPolicy: 'Snapshot',
1643
+ UpdateReplacePolicy: 'Snapshot',
1644
+ Properties: {
1645
+ Engine: 'aurora-postgresql',
1646
+ EngineVersion: dbConfig.engineVersion || '15.3',
1647
+ EngineMode: 'provisioned', // Required for Serverless v2
1648
+ DatabaseName: dbConfig.databaseName || 'frigg_db',
1649
+ MasterUsername: { 'Fn::Sub': '{{resolve:secretsmanager:${FriggDatabaseSecret}:SecretString:username}}' },
1650
+ MasterUserPassword: { 'Fn::Sub': '{{resolve:secretsmanager:${FriggDatabaseSecret}:SecretString:password}}' },
1651
+ DBSubnetGroupName: { Ref: 'FriggDBSubnetGroup' },
1652
+ VpcSecurityGroupIds: [{ Ref: 'FriggAuroraSecurityGroup' }],
1653
+ ServerlessV2ScalingConfiguration: {
1654
+ MinCapacity: dbConfig.scaling?.minCapacity || 0.5,
1655
+ MaxCapacity: dbConfig.scaling?.maxCapacity || 1.0
1656
+ },
1657
+ BackupRetentionPeriod: dbConfig.backupRetentionDays || 7,
1658
+ PreferredBackupWindow: dbConfig.preferredBackupWindow || '03:00-04:00',
1659
+ DeletionProtection: dbConfig.deletionProtection !== false,
1660
+ EnableCloudwatchLogsExports: ['postgresql'],
1661
+ Tags: [
1662
+ { Key: 'Name', Value: '${self:service}-${self:provider.stage}-aurora-cluster' },
1663
+ { Key: 'ManagedBy', Value: 'Frigg' },
1664
+ { Key: 'Service', Value: '${self:service}' },
1665
+ { Key: 'Stage', Value: '${self:provider.stage}' },
1666
+ ]
1667
+ }
1668
+ };
1669
+
1670
+ // 5. Aurora Serverless v2 Instance
1671
+ definition.resources.Resources.FriggAuroraInstance = {
1672
+ Type: 'AWS::RDS::DBInstance',
1673
+ Properties: {
1674
+ Engine: 'aurora-postgresql',
1675
+ DBInstanceClass: 'db.serverless',
1676
+ DBClusterIdentifier: { Ref: 'FriggAuroraCluster' },
1677
+ PubliclyAccessible: false,
1678
+ EnablePerformanceInsights: dbConfig.enablePerformanceInsights || false,
1679
+ Tags: [
1680
+ { Key: 'Name', Value: '${self:service}-${self:provider.stage}-aurora-instance' },
1681
+ { Key: 'ManagedBy', Value: 'Frigg' },
1682
+ { Key: 'Service', Value: '${self:service}' },
1683
+ { Key: 'Stage', Value: '${self:provider.stage}' },
1684
+ ]
1685
+ }
1686
+ };
1687
+
1688
+ // 6. Secret Attachment (links cluster to secret)
1689
+ definition.resources.Resources.FriggSecretAttachment = {
1690
+ Type: 'AWS::SecretsManager::SecretTargetAttachment',
1691
+ Properties: {
1692
+ SecretId: { Ref: 'FriggDatabaseSecret' },
1693
+ TargetId: { Ref: 'FriggAuroraCluster' },
1694
+ TargetType: 'AWS::RDS::DBCluster'
1695
+ }
1696
+ };
1697
+
1698
+ // 7. Add IAM permissions for Secrets Manager
1699
+ definition.provider.iamRoleStatements.push({
1700
+ Effect: 'Allow',
1701
+ Action: [
1702
+ 'secretsmanager:GetSecretValue',
1703
+ 'secretsmanager:DescribeSecret'
1704
+ ],
1705
+ Resource: { Ref: 'FriggDatabaseSecret' }
1706
+ });
1707
+
1708
+ // 8. Set DATABASE_URL environment variable
1709
+ definition.provider.environment.DATABASE_URL = {
1710
+ 'Fn::Sub': [
1711
+ 'postgresql://${Username}:${Password}@${Endpoint}:${Port}/${DatabaseName}',
1712
+ {
1713
+ Username: { 'Fn::Sub': '{{resolve:secretsmanager:${FriggDatabaseSecret}:SecretString:username}}' },
1714
+ Password: { 'Fn::Sub': '{{resolve:secretsmanager:${FriggDatabaseSecret}:SecretString:password}}' },
1715
+ Endpoint: { 'Fn::GetAtt': ['FriggAuroraCluster', 'Endpoint'] },
1716
+ Port: { 'Fn::GetAtt': ['FriggAuroraCluster', 'Port'] },
1717
+ DatabaseName: dbConfig.databaseName || 'frigg_db'
1718
+ }
1719
+ ]
1720
+ };
1721
+
1722
+ // 9. Set DB_TYPE for Prisma client selection
1723
+ definition.provider.environment.DB_TYPE = 'postgresql';
1724
+
1725
+ console.log('āœ… Aurora infrastructure resources created');
1726
+ };
1727
+
1728
+ const useExistingAurora = (definition, AppDefinition, discoveredResources) => {
1729
+ const dbConfig = AppDefinition.database.postgres;
1730
+
1731
+ console.log(`šŸ”— Using existing Aurora cluster: ${discoveredResources.aurora.clusterIdentifier}`);
1732
+
1733
+ // Add IAM permissions for Secrets Manager if secret exists
1734
+ if (discoveredResources.aurora.secretArn) {
1735
+ definition.provider.iamRoleStatements.push({
1736
+ Effect: 'Allow',
1737
+ Action: [
1738
+ 'secretsmanager:GetSecretValue',
1739
+ 'secretsmanager:DescribeSecret'
1740
+ ],
1741
+ Resource: discoveredResources.aurora.secretArn
1742
+ });
1743
+
1744
+ // Set DATABASE_URL from discovered secret
1745
+ definition.provider.environment.DATABASE_URL = {
1746
+ 'Fn::Sub': [
1747
+ 'postgresql://${Username}:${Password}@${Endpoint}:${Port}/${DatabaseName}',
1748
+ {
1749
+ Username: { 'Fn::Sub': `{{resolve:secretsmanager:${discoveredResources.aurora.secretArn}:SecretString:username}}` },
1750
+ Password: { 'Fn::Sub': `{{resolve:secretsmanager:${discoveredResources.aurora.secretArn}:SecretString:password}}` },
1751
+ Endpoint: discoveredResources.aurora.endpoint,
1752
+ Port: discoveredResources.aurora.port,
1753
+ DatabaseName: dbConfig.databaseName || 'frigg_db'
1754
+ }
1755
+ ]
1756
+ };
1757
+ } else if (dbConfig.secretArn) {
1758
+ // Use user-provided secret ARN
1759
+ definition.provider.iamRoleStatements.push({
1760
+ Effect: 'Allow',
1761
+ Action: [
1762
+ 'secretsmanager:GetSecretValue',
1763
+ 'secretsmanager:DescribeSecret'
1764
+ ],
1765
+ Resource: dbConfig.secretArn
1766
+ });
1767
+
1768
+ definition.provider.environment.DATABASE_URL = {
1769
+ 'Fn::Sub': [
1770
+ 'postgresql://${Username}:${Password}@${Endpoint}:${Port}/${DatabaseName}',
1771
+ {
1772
+ Username: { 'Fn::Sub': `{{resolve:secretsmanager:${dbConfig.secretArn}:SecretString:username}}` },
1773
+ Password: { 'Fn::Sub': `{{resolve:secretsmanager:${dbConfig.secretArn}:SecretString:password}}` },
1774
+ Endpoint: discoveredResources.aurora.endpoint,
1775
+ Port: discoveredResources.aurora.port,
1776
+ DatabaseName: dbConfig.databaseName || 'frigg_db'
1777
+ }
1778
+ ]
1779
+ };
1780
+ } else {
1781
+ throw new Error('No database secret found. Provide secretArn in database.postgres configuration or ensure Secrets Manager secret exists.');
1782
+ }
1783
+
1784
+ // Set DB_TYPE for Prisma client selection
1785
+ definition.provider.environment.DB_TYPE = 'postgresql';
1786
+
1787
+ console.log('āœ… Existing Aurora cluster configured');
1788
+ };
1789
+
1790
+ const useDiscoveredAurora = (definition, AppDefinition, discoveredResources) => {
1791
+ console.log(`šŸ” Using discovered Aurora cluster: ${discoveredResources.aurora.clusterIdentifier}`);
1792
+ useExistingAurora(definition, AppDefinition, discoveredResources);
1793
+ };
1794
+
1795
+ const configurePostgres = (definition, AppDefinition, discoveredResources) => {
1796
+ if (!AppDefinition.database?.postgres?.enable) {
1797
+ return;
1798
+ }
1799
+
1800
+ // Validate VPC is enabled (required for Aurora deployment)
1801
+ if (!AppDefinition.vpc?.enable) {
1802
+ throw new Error(
1803
+ 'Aurora PostgreSQL requires VPC deployment. ' +
1804
+ 'Set vpc.enable to true in your app definition.'
1805
+ );
1806
+ }
1807
+
1808
+ // Validate private subnets exist (Aurora requires at least 2 subnets in different AZs)
1809
+ // Skip validation if VPC management is 'create-new' (subnets will be created)
1810
+ const vpcManagement = AppDefinition.vpc?.management || 'discover';
1811
+ if (vpcManagement !== 'create-new' && (!discoveredResources.privateSubnetId1 || !discoveredResources.privateSubnetId2)) {
1812
+ throw new Error(
1813
+ 'Aurora PostgreSQL requires at least 2 private subnets in different availability zones. ' +
1814
+ 'No private subnets were discovered in your VPC. ' +
1815
+ 'Please create private subnets or use VPC management mode "create-new".'
1816
+ );
1817
+ }
1818
+
1819
+ const dbConfig = AppDefinition.database.postgres;
1820
+ const management = dbConfig.management || 'discover';
1821
+
1822
+ console.log(`\n🐘 PostgreSQL Management Mode: ${management}`);
1823
+
1824
+ if (management === 'create-new' || discoveredResources.aurora?.needsCreation) {
1825
+ createAuroraInfrastructure(definition, AppDefinition, discoveredResources);
1826
+ } else if (management === 'use-existing') {
1827
+ if (!discoveredResources.aurora?.clusterIdentifier && !dbConfig.clusterIdentifier) {
1828
+ throw new Error('PostgreSQL management is set to "use-existing" but no clusterIdentifier was found or provided');
1829
+ }
1830
+ useExistingAurora(definition, AppDefinition, discoveredResources);
1831
+ } else {
1832
+ // discover mode
1833
+ if (discoveredResources.aurora?.clusterIdentifier) {
1834
+ useDiscoveredAurora(definition, AppDefinition, discoveredResources);
1835
+ } else {
1836
+ throw new Error('No Aurora cluster found in discovery mode. Set management to "create-new" or provide clusterIdentifier with "use-existing".');
1837
+ }
1838
+ }
1839
+ };
1840
+
1530
1841
  const configureSsm = (definition, AppDefinition) => {
1531
1842
  if (AppDefinition.ssm?.enable !== true) {
1532
1843
  return;
@@ -1634,6 +1945,7 @@ const composeServerlessDefinition = async (AppDefinition) => {
1634
1945
 
1635
1946
  applyKmsConfiguration(definition, AppDefinition, discoveredResources);
1636
1947
  configureVpc(definition, AppDefinition, discoveredResources);
1948
+ configurePostgres(definition, AppDefinition, discoveredResources);
1637
1949
  configureSsm(definition, AppDefinition);
1638
1950
  attachIntegrations(definition, AppDefinition);
1639
1951
  configureWebsockets(definition, AppDefinition);
package/package.json CHANGED
@@ -1,16 +1,18 @@
1
1
  {
2
2
  "name": "@friggframework/devtools",
3
3
  "prettier": "@friggframework/prettier-config",
4
- "version": "2.0.0--canary.454.a254930.0",
4
+ "version": "2.0.0--canary.454.726ab78.0",
5
5
  "dependencies": {
6
6
  "@aws-sdk/client-ec2": "^3.835.0",
7
7
  "@aws-sdk/client-kms": "^3.835.0",
8
+ "@aws-sdk/client-rds": "^3.906.0",
9
+ "@aws-sdk/client-secrets-manager": "^3.906.0",
8
10
  "@aws-sdk/client-sts": "^3.835.0",
9
11
  "@babel/eslint-parser": "^7.18.9",
10
12
  "@babel/parser": "^7.25.3",
11
13
  "@babel/traverse": "^7.25.3",
12
- "@friggframework/schemas": "2.0.0--canary.454.a254930.0",
13
- "@friggframework/test": "2.0.0--canary.454.a254930.0",
14
+ "@friggframework/schemas": "2.0.0--canary.454.726ab78.0",
15
+ "@friggframework/test": "2.0.0--canary.454.726ab78.0",
14
16
  "@hapi/boom": "^10.0.1",
15
17
  "@inquirer/prompts": "^5.3.8",
16
18
  "axios": "^1.7.2",
@@ -32,8 +34,8 @@
32
34
  "serverless-http": "^2.7.0"
33
35
  },
34
36
  "devDependencies": {
35
- "@friggframework/eslint-config": "2.0.0--canary.454.a254930.0",
36
- "@friggframework/prettier-config": "2.0.0--canary.454.a254930.0",
37
+ "@friggframework/eslint-config": "2.0.0--canary.454.726ab78.0",
38
+ "@friggframework/prettier-config": "2.0.0--canary.454.726ab78.0",
37
39
  "aws-sdk-client-mock": "^4.1.0",
38
40
  "aws-sdk-client-mock-jest": "^4.1.0",
39
41
  "jest": "^30.1.3",
@@ -68,5 +70,5 @@
68
70
  "publishConfig": {
69
71
  "access": "public"
70
72
  },
71
- "gitHead": "a2549302c9b8c1aae833c1ca7d3d8e2b3fb6f3a3"
73
+ "gitHead": "726ab78ba26d816f9aadab7e26c1e0495da5c2e7"
72
74
  }