@friggframework/devtools 2.0.0--canary.463.62579dd.0 → 2.0.0--canary.461.84ff4f5.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.
@@ -1,6 +1,7 @@
1
1
  const path = require('path');
2
2
  const fs = require('fs');
3
3
  const { AWSDiscovery } = require('./aws-discovery');
4
+ const { buildPrismaLayer } = require('./scripts/build-prisma-layer');
4
5
 
5
6
  const shouldRunDiscovery = (AppDefinition) => {
6
7
  console.log(
@@ -17,7 +18,8 @@ const shouldRunDiscovery = (AppDefinition) => {
17
18
  return (
18
19
  AppDefinition.vpc?.enable === true ||
19
20
  AppDefinition.encryption?.fieldLevelEncryptionMethod === 'kms' ||
20
- AppDefinition.ssm?.enable === true
21
+ AppDefinition.ssm?.enable === true ||
22
+ AppDefinition.database?.postgres?.enable === true
21
23
  );
22
24
  };
23
25
 
@@ -68,8 +70,7 @@ const getAppEnvironmentVars = (AppDefinition) => {
68
70
  }
69
71
  if (skippedKeys.length > 0) {
70
72
  console.log(
71
- ` ⚠️ Skipped ${
72
- skippedKeys.length
73
+ ` ⚠️ Skipped ${skippedKeys.length
73
74
  } reserved AWS Lambda variables: ${skippedKeys.join(', ')}`
74
75
  );
75
76
  }
@@ -544,10 +545,18 @@ const gatherDiscoveredResources = async (AppDefinition) => {
544
545
  try {
545
546
  const region = process.env.AWS_REGION || 'us-east-1';
546
547
  const discovery = new AWSDiscovery(region);
548
+ // Use Serverless Framework's stage resolution (opt:stage with 'dev' as default)
549
+ // This matches how serverless.yml resolves ${opt:stage, "dev"}
550
+ // IMPORTANT: Use SLS_STAGE (not STAGE) to match actual deployment stage
551
+ const stage = process.env.SLS_STAGE || 'dev';
552
+
547
553
  const config = {
548
554
  vpc: AppDefinition.vpc || {},
549
555
  encryption: AppDefinition.encryption || {},
550
556
  ssm: AppDefinition.ssm || {},
557
+ database: AppDefinition.database || {},
558
+ serviceName: AppDefinition.name || 'create-frigg-app',
559
+ stage: stage,
551
560
  };
552
561
 
553
562
  const discoveredResources = await discovery.discoverResources(config);
@@ -603,6 +612,22 @@ const buildEnvironment = (appEnvironmentVars, discoveredResources) => {
603
612
  }
604
613
  }
605
614
 
615
+ // Add Aurora discovery mappings
616
+ if (discoveredResources.aurora) {
617
+ if (discoveredResources.aurora.clusterIdentifier) {
618
+ environment.AWS_DISCOVERY_AURORA_CLUSTER_ID = discoveredResources.aurora.clusterIdentifier;
619
+ }
620
+ if (discoveredResources.aurora.endpoint) {
621
+ environment.AWS_DISCOVERY_AURORA_ENDPOINT = discoveredResources.aurora.endpoint;
622
+ }
623
+ if (discoveredResources.aurora.port) {
624
+ environment.AWS_DISCOVERY_AURORA_PORT = discoveredResources.aurora.port.toString();
625
+ }
626
+ if (discoveredResources.aurora.secretArn) {
627
+ environment.AWS_DISCOVERY_AURORA_SECRET_ARN = discoveredResources.aurora.secretArn;
628
+ }
629
+ }
630
+
606
631
  return environment;
607
632
  };
608
633
 
@@ -613,20 +638,72 @@ const createBaseDefinition = (
613
638
  ) => {
614
639
  const region = process.env.AWS_REGION || 'us-east-1';
615
640
 
641
+ // Function-level package config to exclude Prisma and AWS SDK
642
+ // Uses native Serverless package.exclude since jetpack function-level config isn't supported in v3
643
+ const functionPackageConfig = {
644
+ exclude: [
645
+ // Exclude AWS SDK (already in Lambda runtime)
646
+ 'node_modules/aws-sdk/**',
647
+ 'node_modules/@aws-sdk/**',
648
+
649
+ // Exclude Prisma (provided via Lambda Layer)
650
+ 'node_modules/@prisma/**',
651
+ 'node_modules/.prisma/**',
652
+ 'node_modules/prisma/**',
653
+ 'node_modules/@friggframework/core/generated/**',
654
+ ],
655
+ };
656
+
616
657
  return {
617
658
  frameworkVersion: '>=3.17.0',
618
659
  service: AppDefinition.name || 'create-frigg-app',
619
660
  package: {
620
661
  individually: true,
621
- exclude: [
622
- '!**/node_modules/aws-sdk/**',
623
- '!**/node_modules/@aws-sdk/**',
624
- '!package.json',
662
+ // NOTE: These patterns are NOT used when serverless-jetpack is enabled with trace mode
663
+ // Jetpack's trace mode completely overrides package.patterns during dependency resolution
664
+ // These are kept commented out as a fallback if Jetpack needs to be disabled
665
+ patterns: [
666
+ // AWS SDK exclusions (already in Lambda runtime)
667
+ // '!**/node_modules/aws-sdk/**',
668
+ // '!**/node_modules/@aws-sdk/**',
669
+
670
+ // Prisma exclusions (provided via Lambda Layer)
671
+ // '!**/node_modules/@prisma/**',
672
+ // '!**/node_modules/.prisma/**',
673
+ // '!**/node_modules/@prisma-mongodb/**',
674
+ // '!**/node_modules/@prisma-postgresql/**',
675
+ // '!**/node_modules/prisma/**',
676
+
677
+ // Exclude Prisma generated clients from @friggframework/core
678
+ // '!**/node_modules/@friggframework/core/generated/**',
679
+
680
+ // Exclude development and test files
681
+ // '!**/test/**',
682
+ // '!**/tests/**',
683
+ // '!**/*.test.js',
684
+ // '!**/*.spec.js',
685
+ // '!**/*.map',
686
+ // '!**/jest.config.js',
687
+ // '!**/jest.unit.config.js',
688
+ // '!**/.eslintrc.json',
689
+ // '!**/.prettierrc',
690
+ // '!**/.prettierignore',
691
+ // '!**/.markdownlintignore',
692
+ // '!**/docker-compose.yml',
693
+ // '!**/package.json',
694
+ // '!**/README.md',
695
+ // '!**/*.md',
696
+
697
+ // Exclude .DS_Store and other OS files
698
+ // '!**/.DS_Store',
699
+ // '!**/.git/**',
700
+ // '!**/.claude-flow/**',
625
701
  ],
626
702
  },
627
703
  useDotenv: true,
628
704
  provider: {
629
705
  name: AppDefinition.provider || 'aws',
706
+ ...(process.env.AWS_PROFILE && { profile: process.env.AWS_PROFILE }),
630
707
  runtime: 'nodejs20.x',
631
708
  timeout: 30,
632
709
  region,
@@ -697,13 +774,17 @@ const createBaseDefinition = (
697
774
  skipCacheInvalidation: false,
698
775
  },
699
776
  jetpack: {
700
- base: '..',
777
+ base: '..', // Essential for reaching handlers in node_modules/@friggframework
778
+ // NOTE: Service-level preInclude applies to EVERYTHING (functions + layers)
779
+ // We need to ONLY exclude from functions, not from the Prisma layer
780
+ // Solution: Apply exclusions at function level instead
701
781
  },
702
782
  },
703
783
  functions: {
704
784
  auth: {
705
- handler:
706
- 'node_modules/@friggframework/core/handlers/routers/auth.handler',
785
+ handler: 'node_modules/@friggframework/core/handlers/routers/auth.handler',
786
+ layers: [{ Ref: 'PrismaLambdaLayer' }],
787
+ package: functionPackageConfig,
707
788
  events: [
708
789
  { httpApi: { path: '/api/integrations', method: 'ANY' } },
709
790
  {
@@ -716,20 +797,52 @@ const createBaseDefinition = (
716
797
  ],
717
798
  },
718
799
  user: {
719
- handler:
720
- 'node_modules/@friggframework/core/handlers/routers/user.handler',
721
- events: [
722
- { httpApi: { path: '/user/{proxy+}', method: 'ANY' } },
723
- ],
800
+ handler: 'node_modules/@friggframework/core/handlers/routers/user.handler',
801
+ layers: [{ Ref: 'PrismaLambdaLayer' }],
802
+ package: functionPackageConfig,
803
+ events: [{ httpApi: { path: '/user/{proxy+}', method: 'ANY' } }],
724
804
  },
725
805
  health: {
726
- handler:
727
- 'node_modules/@friggframework/core/handlers/routers/health.handler',
806
+ handler: 'node_modules/@friggframework/core/handlers/routers/health.handler',
807
+ layers: [{ Ref: 'PrismaLambdaLayer' }],
808
+ package: functionPackageConfig,
728
809
  events: [
729
810
  { httpApi: { path: '/health', method: 'GET' } },
730
811
  { httpApi: { path: '/health/{proxy+}', method: 'GET' } },
731
812
  ],
732
813
  },
814
+ dbMigrate: {
815
+ handler: 'node_modules/@friggframework/core/handlers/workers/db-migration.handler',
816
+ // Uses Prisma Layer (includes CLI) - simpler than standalone packaging
817
+ layers: [{ Ref: 'PrismaLambdaLayer' }],
818
+ timeout: 300, // 5 minutes for long-running migrations
819
+ memorySize: 512, // Extra memory for Prisma CLI operations
820
+ reservedConcurrency: 1, // Prevent concurrent migrations
821
+ description: 'Runs database migrations via Prisma (invoke manually from CI/CD). Uses Prisma layer with CLI.',
822
+ package: functionPackageConfig, // Use same exclusions as other functions
823
+ // No events - this function is invoked manually via AWS CLI
824
+ maximumEventAge: 60, // Don't retry old migration requests (60 seconds)
825
+ maximumRetryAttempts: 0, // Don't auto-retry failed migrations
826
+ tags: {
827
+ Purpose: 'DatabaseMigration',
828
+ ManagedBy: 'Frigg',
829
+ },
830
+ // Environment variables for non-interactive Prisma CLI operation
831
+ environment: {
832
+ CI: '1', // Forces Prisma to non-interactive mode
833
+ PRISMA_HIDE_UPDATE_MESSAGE: '1', // Suppress update messages
834
+ PRISMA_MIGRATE_SKIP_SEED: '1', // Skip seeding during migrations
835
+ },
836
+ },
837
+ },
838
+ layers: {
839
+ prisma: {
840
+ path: 'layers/prisma',
841
+ name: '${self:service}-prisma-${sls:stage}',
842
+ description: 'Prisma ORM client with CLI and rhel-openssl-3.0.x binaries. Configured based on AppDefinition database settings. Used by all functions.',
843
+ compatibleRuntimes: ['nodejs18.x', 'nodejs20.x'],
844
+ retain: false, // Don't retain old layer versions
845
+ },
733
846
  },
734
847
  resources: {
735
848
  Resources: {
@@ -828,18 +941,22 @@ const applyKmsConfiguration = (
828
941
  }
829
942
 
830
943
  if (discoveredResources.defaultKmsKeyId) {
831
- console.log(
832
- `Using existing KMS key: ${discoveredResources.defaultKmsKeyId}`
833
- );
834
- definition.resources.Resources.FriggKMSKeyAlias = {
835
- Type: 'AWS::KMS::Alias',
836
- DeletionPolicy: 'Retain',
837
- Properties: {
838
- AliasName:
839
- 'alias/${self:service}-${self:provider.stage}-frigg-kms',
840
- TargetKeyId: discoveredResources.defaultKmsKeyId,
841
- },
842
- };
944
+ console.log(`Using existing KMS key: ${discoveredResources.defaultKmsKeyId}`);
945
+
946
+ // Only create alias if it doesn't already exist
947
+ if (!discoveredResources.kmsAliasExists) {
948
+ console.log('Creating KMS alias for discovered key...');
949
+ definition.resources.Resources.FriggKMSKeyAlias = {
950
+ Type: 'AWS::KMS::Alias',
951
+ DeletionPolicy: 'Retain',
952
+ Properties: {
953
+ AliasName: 'alias/${self:service}-${self:provider.stage}-frigg-kms',
954
+ TargetKeyId: discoveredResources.defaultKmsKeyId,
955
+ },
956
+ };
957
+ } else {
958
+ console.log('KMS alias already exists, skipping alias creation');
959
+ }
843
960
 
844
961
  definition.provider.iamRoleStatements.push({
845
962
  Effect: 'Allow',
@@ -850,7 +967,7 @@ const applyKmsConfiguration = (
850
967
  if (AppDefinition.encryption?.createResourceIfNoneFound !== true) {
851
968
  throw new Error(
852
969
  'KMS field-level encryption is enabled but no KMS key was found. ' +
853
- 'Either provide an existing KMS key or set encryption.createResourceIfNoneFound to true to create a new key.'
970
+ 'Either provide an existing KMS key or set encryption.createResourceIfNoneFound to true to create a new key.'
854
971
  );
855
972
  }
856
973
 
@@ -889,9 +1006,8 @@ const applyKmsConfiguration = (
889
1006
  Resource: '*',
890
1007
  Condition: {
891
1008
  StringEquals: {
892
- 'kms:ViaService': `lambda.${
893
- process.env.AWS_REGION || 'us-east-1'
894
- }.amazonaws.com`,
1009
+ 'kms:ViaService': `lambda.${process.env.AWS_REGION || 'us-east-1'
1010
+ }.amazonaws.com`,
895
1011
  },
896
1012
  },
897
1013
  },
@@ -1293,13 +1409,13 @@ const configureVpc = (definition, AppDefinition, discoveredResources) => {
1293
1409
  };
1294
1410
 
1295
1411
  definition.resources.Resources.FriggPublicSubnetRouteTableAssociation =
1296
- {
1297
- Type: 'AWS::EC2::SubnetRouteTableAssociation',
1298
- Properties: {
1299
- SubnetId: { Ref: 'FriggPublicSubnet' },
1300
- RouteTableId: { Ref: 'FriggPublicRouteTable' },
1301
- },
1302
- };
1412
+ {
1413
+ Type: 'AWS::EC2::SubnetRouteTableAssociation',
1414
+ Properties: {
1415
+ SubnetId: { Ref: 'FriggPublicSubnet' },
1416
+ RouteTableId: { Ref: 'FriggPublicRouteTable' },
1417
+ },
1418
+ };
1303
1419
 
1304
1420
  definition.resources.Resources.FriggLambdaRouteTable = {
1305
1421
  Type: 'AWS::EC2::RouteTable',
@@ -1316,22 +1432,22 @@ const configureVpc = (definition, AppDefinition, discoveredResources) => {
1316
1432
  };
1317
1433
 
1318
1434
  definition.resources.Resources.FriggPrivateSubnet1RouteTableAssociation =
1319
- {
1320
- Type: 'AWS::EC2::SubnetRouteTableAssociation',
1321
- Properties: {
1322
- SubnetId: { Ref: 'FriggPrivateSubnet1' },
1323
- RouteTableId: { Ref: 'FriggLambdaRouteTable' },
1324
- },
1325
- };
1435
+ {
1436
+ Type: 'AWS::EC2::SubnetRouteTableAssociation',
1437
+ Properties: {
1438
+ SubnetId: { Ref: 'FriggPrivateSubnet1' },
1439
+ RouteTableId: { Ref: 'FriggLambdaRouteTable' },
1440
+ },
1441
+ };
1326
1442
 
1327
1443
  definition.resources.Resources.FriggPrivateSubnet2RouteTableAssociation =
1328
- {
1329
- Type: 'AWS::EC2::SubnetRouteTableAssociation',
1330
- Properties: {
1331
- SubnetId: { Ref: 'FriggPrivateSubnet2' },
1332
- RouteTableId: { Ref: 'FriggLambdaRouteTable' },
1333
- },
1334
- };
1444
+ {
1445
+ Type: 'AWS::EC2::SubnetRouteTableAssociation',
1446
+ Properties: {
1447
+ SubnetId: { Ref: 'FriggPrivateSubnet2' },
1448
+ RouteTableId: { Ref: 'FriggLambdaRouteTable' },
1449
+ },
1450
+ };
1335
1451
  }
1336
1452
  } else if (subnetManagement === 'use-existing') {
1337
1453
  if (
@@ -1348,12 +1464,12 @@ const configureVpc = (definition, AppDefinition, discoveredResources) => {
1348
1464
  AppDefinition.vpc.subnets?.ids?.length > 0
1349
1465
  ? AppDefinition.vpc.subnets.ids
1350
1466
  : discoveredResources.privateSubnetId1 &&
1351
- discoveredResources.privateSubnetId2
1352
- ? [
1353
- discoveredResources.privateSubnetId1,
1354
- discoveredResources.privateSubnetId2,
1355
- ]
1356
- : [];
1467
+ discoveredResources.privateSubnetId2
1468
+ ? [
1469
+ discoveredResources.privateSubnetId1,
1470
+ discoveredResources.privateSubnetId2,
1471
+ ]
1472
+ : [];
1357
1473
 
1358
1474
  if (vpcConfig.subnetIds.length < 2) {
1359
1475
  if (AppDefinition.vpc.selfHeal) {
@@ -1560,13 +1676,13 @@ const configureVpc = (definition, AppDefinition, discoveredResources) => {
1560
1676
  };
1561
1677
 
1562
1678
  definition.resources.Resources.FriggPublicSubnetRouteTableAssociation =
1563
- {
1564
- Type: 'AWS::EC2::SubnetRouteTableAssociation',
1565
- Properties: {
1566
- SubnetId: { Ref: 'FriggPublicSubnet' },
1567
- RouteTableId: { Ref: 'FriggPublicRouteTable' },
1568
- },
1569
- };
1679
+ {
1680
+ Type: 'AWS::EC2::SubnetRouteTableAssociation',
1681
+ Properties: {
1682
+ SubnetId: { Ref: 'FriggPublicSubnet' },
1683
+ RouteTableId: { Ref: 'FriggPublicRouteTable' },
1684
+ },
1685
+ };
1570
1686
  }
1571
1687
 
1572
1688
  definition.resources.Resources.FriggNATGateway = {
@@ -1577,11 +1693,11 @@ const configureVpc = (definition, AppDefinition, discoveredResources) => {
1577
1693
  AllocationId: useExistingEip
1578
1694
  ? discoveredResources.existingElasticIpAllocationId
1579
1695
  : {
1580
- 'Fn::GetAtt': [
1581
- 'FriggNATGatewayEIP',
1582
- 'AllocationId',
1583
- ],
1584
- },
1696
+ 'Fn::GetAtt': [
1697
+ 'FriggNATGatewayEIP',
1698
+ 'AllocationId',
1699
+ ],
1700
+ },
1585
1701
  SubnetId: discoveredResources.publicSubnetId || {
1586
1702
  Ref: 'FriggPublicSubnet',
1587
1703
  },
@@ -1894,7 +2010,9 @@ const configureVpc = (definition, AppDefinition, discoveredResources) => {
1894
2010
  },
1895
2011
  };
1896
2012
 
1897
- if (AppDefinition.secretsManager?.enable === true) {
2013
+ // Create Secrets Manager VPC Endpoint if explicitly enabled OR if Aurora is enabled
2014
+ // (Aurora requires Secrets Manager access for credential retrieval)
2015
+ if (AppDefinition.secretsManager?.enable === true || AppDefinition.database?.postgres?.enable === true) {
1898
2016
  definition.resources.Resources.VPCEndpointSecretsManager = {
1899
2017
  Type: 'AWS::EC2::VPCEndpoint',
1900
2018
  Properties: {
@@ -1912,6 +2030,364 @@ const configureVpc = (definition, AppDefinition, discoveredResources) => {
1912
2030
  }
1913
2031
  };
1914
2032
 
2033
+ const createAuroraInfrastructure = (definition, AppDefinition, discoveredResources) => {
2034
+ const dbConfig = AppDefinition.database.postgres;
2035
+
2036
+ console.log('🔧 Creating Aurora Serverless v2 infrastructure...');
2037
+
2038
+ // 1. DB Subnet Group (using Lambda private subnets)
2039
+ definition.resources.Resources.FriggDBSubnetGroup = {
2040
+ Type: 'AWS::RDS::DBSubnetGroup',
2041
+ Properties: {
2042
+ DBSubnetGroupDescription: 'Subnet group for Frigg Aurora cluster',
2043
+ SubnetIds: [
2044
+ discoveredResources.privateSubnetId1,
2045
+ discoveredResources.privateSubnetId2
2046
+ ],
2047
+ Tags: [
2048
+ { Key: 'Name', Value: '${self:service}-${self:provider.stage}-db-subnet-group' },
2049
+ { Key: 'ManagedBy', Value: 'Frigg' },
2050
+ { Key: 'Service', Value: '${self:service}' },
2051
+ { Key: 'Stage', Value: '${self:provider.stage}' },
2052
+ ]
2053
+ }
2054
+ };
2055
+
2056
+ // 2. Security Group (allow Lambda SG to access 5432)
2057
+ // In create-new VPC mode, Lambda uses FriggLambdaSecurityGroup
2058
+ // In other modes, use discovered default security group
2059
+ const lambdaSecurityGroupId = AppDefinition.vpc?.management === 'create-new'
2060
+ ? { Ref: 'FriggLambdaSecurityGroup' }
2061
+ : discoveredResources.defaultSecurityGroupId;
2062
+
2063
+ definition.resources.Resources.FriggAuroraSecurityGroup = {
2064
+ Type: 'AWS::EC2::SecurityGroup',
2065
+ Properties: {
2066
+ GroupDescription: 'Security group for Frigg Aurora PostgreSQL',
2067
+ VpcId: discoveredResources.defaultVpcId,
2068
+ SecurityGroupIngress: [
2069
+ {
2070
+ IpProtocol: 'tcp',
2071
+ FromPort: 5432,
2072
+ ToPort: 5432,
2073
+ SourceSecurityGroupId: lambdaSecurityGroupId,
2074
+ Description: 'PostgreSQL access from Lambda functions'
2075
+ }
2076
+ ],
2077
+ Tags: [
2078
+ { Key: 'Name', Value: '${self:service}-${self:provider.stage}-aurora-sg' },
2079
+ { Key: 'ManagedBy', Value: 'Frigg' },
2080
+ { Key: 'Service', Value: '${self:service}' },
2081
+ { Key: 'Stage', Value: '${self:provider.stage}' },
2082
+ ]
2083
+ }
2084
+ };
2085
+
2086
+ // 3. Secrets Manager Secret (database credentials)
2087
+ definition.resources.Resources.FriggDatabaseSecret = {
2088
+ Type: 'AWS::SecretsManager::Secret',
2089
+ Properties: {
2090
+ Name: '${self:service}-${self:provider.stage}-aurora-credentials',
2091
+ Description: 'Aurora PostgreSQL credentials for Frigg application',
2092
+ GenerateSecretString: {
2093
+ SecretStringTemplate: JSON.stringify({
2094
+ username: dbConfig.masterUsername || 'frigg_admin'
2095
+ }),
2096
+ GenerateStringKey: 'password',
2097
+ PasswordLength: 32,
2098
+ ExcludeCharacters: '"@/\\'
2099
+ },
2100
+ Tags: [
2101
+ { Key: 'ManagedBy', Value: 'Frigg' },
2102
+ { Key: 'Service', Value: '${self:service}' },
2103
+ { Key: 'Stage', Value: '${self:provider.stage}' },
2104
+ ]
2105
+ }
2106
+ };
2107
+
2108
+ // 4. Aurora Serverless v2 Cluster
2109
+ definition.resources.Resources.FriggAuroraCluster = {
2110
+ Type: 'AWS::RDS::DBCluster',
2111
+ DeletionPolicy: 'Snapshot',
2112
+ UpdateReplacePolicy: 'Snapshot',
2113
+ Properties: {
2114
+ Engine: 'aurora-postgresql',
2115
+ EngineVersion: dbConfig.engineVersion || '15.3',
2116
+ EngineMode: 'provisioned', // Required for Serverless v2
2117
+ DatabaseName: dbConfig.databaseName || 'frigg_db',
2118
+ MasterUsername: { 'Fn::Sub': '{{resolve:secretsmanager:${FriggDatabaseSecret}:SecretString:username}}' },
2119
+ MasterUserPassword: { 'Fn::Sub': '{{resolve:secretsmanager:${FriggDatabaseSecret}:SecretString:password}}' },
2120
+ DBSubnetGroupName: { Ref: 'FriggDBSubnetGroup' },
2121
+ VpcSecurityGroupIds: [{ Ref: 'FriggAuroraSecurityGroup' }],
2122
+ ServerlessV2ScalingConfiguration: {
2123
+ MinCapacity: dbConfig.scaling?.minCapacity || 0.5,
2124
+ MaxCapacity: dbConfig.scaling?.maxCapacity || 1.0
2125
+ },
2126
+ BackupRetentionPeriod: dbConfig.backupRetentionDays || 7,
2127
+ PreferredBackupWindow: dbConfig.preferredBackupWindow || '03:00-04:00',
2128
+ DeletionProtection: dbConfig.deletionProtection !== false,
2129
+ EnableCloudwatchLogsExports: ['postgresql'],
2130
+ Tags: [
2131
+ { Key: 'Name', Value: '${self:service}-${self:provider.stage}-aurora-cluster' },
2132
+ { Key: 'ManagedBy', Value: 'Frigg' },
2133
+ { Key: 'Service', Value: '${self:service}' },
2134
+ { Key: 'Stage', Value: '${self:provider.stage}' },
2135
+ ]
2136
+ }
2137
+ };
2138
+
2139
+ // 5. Aurora Serverless v2 Instance
2140
+ definition.resources.Resources.FriggAuroraInstance = {
2141
+ Type: 'AWS::RDS::DBInstance',
2142
+ Properties: {
2143
+ Engine: 'aurora-postgresql',
2144
+ DBInstanceClass: 'db.serverless',
2145
+ DBClusterIdentifier: { Ref: 'FriggAuroraCluster' },
2146
+ PubliclyAccessible: false,
2147
+ EnablePerformanceInsights: dbConfig.enablePerformanceInsights || false,
2148
+ Tags: [
2149
+ { Key: 'Name', Value: '${self:service}-${self:provider.stage}-aurora-instance' },
2150
+ { Key: 'ManagedBy', Value: 'Frigg' },
2151
+ { Key: 'Service', Value: '${self:service}' },
2152
+ { Key: 'Stage', Value: '${self:provider.stage}' },
2153
+ ]
2154
+ }
2155
+ };
2156
+
2157
+ // 6. Secret Attachment (links cluster to secret)
2158
+ definition.resources.Resources.FriggSecretAttachment = {
2159
+ Type: 'AWS::SecretsManager::SecretTargetAttachment',
2160
+ Properties: {
2161
+ SecretId: { Ref: 'FriggDatabaseSecret' },
2162
+ TargetId: { Ref: 'FriggAuroraCluster' },
2163
+ TargetType: 'AWS::RDS::DBCluster'
2164
+ }
2165
+ };
2166
+
2167
+ // 7. Add IAM permissions for Secrets Manager
2168
+ definition.provider.iamRoleStatements.push({
2169
+ Effect: 'Allow',
2170
+ Action: [
2171
+ 'secretsmanager:GetSecretValue',
2172
+ 'secretsmanager:DescribeSecret'
2173
+ ],
2174
+ Resource: { Ref: 'FriggDatabaseSecret' }
2175
+ });
2176
+
2177
+ // 8. Set DATABASE_URL environment variable
2178
+ definition.provider.environment.DATABASE_URL = {
2179
+ 'Fn::Sub': [
2180
+ 'postgresql://${Username}:${Password}@${Endpoint}:${Port}/${DatabaseName}',
2181
+ {
2182
+ Username: { 'Fn::Sub': '{{resolve:secretsmanager:${FriggDatabaseSecret}:SecretString:username}}' },
2183
+ Password: { 'Fn::Sub': '{{resolve:secretsmanager:${FriggDatabaseSecret}:SecretString:password}}' },
2184
+ Endpoint: { 'Fn::GetAtt': ['FriggAuroraCluster', 'Endpoint'] },
2185
+ Port: { 'Fn::GetAtt': ['FriggAuroraCluster', 'Port'] },
2186
+ DatabaseName: dbConfig.databaseName || 'frigg_db'
2187
+ }
2188
+ ]
2189
+ };
2190
+
2191
+ // 9. Set DB_TYPE for Prisma client selection
2192
+ definition.provider.environment.DB_TYPE = 'postgresql';
2193
+
2194
+ console.log('✅ Aurora infrastructure resources created');
2195
+ };
2196
+
2197
+ const useExistingAurora = (definition, AppDefinition, discoveredResources) => {
2198
+ const dbConfig = AppDefinition.database.postgres;
2199
+ const selfHeal = AppDefinition.database?.postgres?.selfHeal !== false; // Default to true
2200
+
2201
+ console.log(`🔗 Using existing Aurora cluster: ${discoveredResources.aurora.clusterIdentifier}`);
2202
+ console.log(`[DEBUG] discoveredResources.aurora.isFriggManaged: ${discoveredResources.aurora.isFriggManaged}`);
2203
+ console.log(`[DEBUG] selfHeal: ${selfHeal}`);
2204
+ console.log(`[DEBUG] discoveredResources.aurora.secretArn: ${discoveredResources.aurora.secretArn}`);
2205
+ console.log(`[DEBUG] dbConfig.secretArn: ${dbConfig.secretArn}`);
2206
+
2207
+ // Add IAM permissions for Secrets Manager if secret exists
2208
+ if (discoveredResources.aurora.secretArn) {
2209
+ definition.provider.iamRoleStatements.push({
2210
+ Effect: 'Allow',
2211
+ Action: [
2212
+ 'secretsmanager:GetSecretValue',
2213
+ 'secretsmanager:DescribeSecret'
2214
+ ],
2215
+ Resource: discoveredResources.aurora.secretArn
2216
+ });
2217
+
2218
+ // Set DATABASE_URL from discovered secret
2219
+ definition.provider.environment.DATABASE_URL = {
2220
+ 'Fn::Sub': [
2221
+ 'postgresql://${Username}:${Password}@${Endpoint}:${Port}/${DatabaseName}',
2222
+ {
2223
+ Username: { 'Fn::Sub': `{{resolve:secretsmanager:${discoveredResources.aurora.secretArn}:SecretString:username}}` },
2224
+ Password: { 'Fn::Sub': `{{resolve:secretsmanager:${discoveredResources.aurora.secretArn}:SecretString:password}}` },
2225
+ Endpoint: discoveredResources.aurora.endpoint,
2226
+ Port: discoveredResources.aurora.port,
2227
+ DatabaseName: dbConfig.databaseName || 'frigg_db'
2228
+ }
2229
+ ]
2230
+ };
2231
+ } else if (dbConfig.secretArn) {
2232
+ // Use user-provided secret ARN
2233
+ definition.provider.iamRoleStatements.push({
2234
+ Effect: 'Allow',
2235
+ Action: [
2236
+ 'secretsmanager:GetSecretValue',
2237
+ 'secretsmanager:DescribeSecret'
2238
+ ],
2239
+ Resource: dbConfig.secretArn
2240
+ });
2241
+
2242
+ definition.provider.environment.DATABASE_URL = {
2243
+ 'Fn::Sub': [
2244
+ 'postgresql://${Username}:${Password}@${Endpoint}:${Port}/${DatabaseName}',
2245
+ {
2246
+ Username: { 'Fn::Sub': `{{resolve:secretsmanager:${dbConfig.secretArn}:SecretString:username}}` },
2247
+ Password: { 'Fn::Sub': `{{resolve:secretsmanager:${dbConfig.secretArn}:SecretString:password}}` },
2248
+ Endpoint: discoveredResources.aurora.endpoint,
2249
+ Port: discoveredResources.aurora.port,
2250
+ DatabaseName: dbConfig.databaseName || 'frigg_db'
2251
+ }
2252
+ ]
2253
+ };
2254
+ } else if (selfHeal && discoveredResources.aurora?.isFriggManaged) {
2255
+ // Self-healing mode: recreate missing secret for Frigg-managed cluster
2256
+ console.log('⚠️ No database secret found for Frigg-managed cluster');
2257
+ console.log('🔧 Self-healing enabled: Creating new database secret with automatic password rotation');
2258
+
2259
+ // Get the current master username from the cluster
2260
+ const currentUsername = discoveredResources.aurora.masterUsername || dbConfig.masterUsername || 'frigg_admin';
2261
+
2262
+ // Create Secrets Manager Secret (database credentials)
2263
+ // Note: We generate a NEW password, which will be synced to the cluster via SecretTargetAttachment
2264
+ definition.resources.Resources.FriggDatabaseSecret = {
2265
+ Type: 'AWS::SecretsManager::Secret',
2266
+ Properties: {
2267
+ Name: '${self:service}-${self:provider.stage}-aurora-credentials',
2268
+ Description: 'Aurora PostgreSQL credentials for Frigg application (auto-healed)',
2269
+ GenerateSecretString: {
2270
+ SecretStringTemplate: JSON.stringify({
2271
+ username: currentUsername
2272
+ }),
2273
+ GenerateStringKey: 'password',
2274
+ PasswordLength: 32,
2275
+ ExcludeCharacters: '"@/\\`\''
2276
+ },
2277
+ Tags: [
2278
+ { Key: 'ManagedBy', Value: 'Frigg' },
2279
+ { Key: 'Service', Value: '${self:service}' },
2280
+ { Key: 'Stage', Value: '${self:provider.stage}' },
2281
+ { Key: 'AutoHealed', Value: 'true' }
2282
+ ]
2283
+ }
2284
+ };
2285
+
2286
+ // Create SecretTargetAttachment to link secret to existing cluster
2287
+ // This will automatically rotate the cluster password to match the secret!
2288
+ definition.resources.Resources.FriggSecretAttachment = {
2289
+ Type: 'AWS::SecretsManager::SecretTargetAttachment',
2290
+ Properties: {
2291
+ SecretId: { Ref: 'FriggDatabaseSecret' },
2292
+ TargetId: discoveredResources.aurora.clusterIdentifier,
2293
+ TargetType: 'AWS::RDS::DBCluster'
2294
+ }
2295
+ };
2296
+
2297
+ // Add IAM permissions for the new secret
2298
+ definition.provider.iamRoleStatements.push({
2299
+ Effect: 'Allow',
2300
+ Action: [
2301
+ 'secretsmanager:GetSecretValue',
2302
+ 'secretsmanager:DescribeSecret'
2303
+ ],
2304
+ Resource: { Ref: 'FriggDatabaseSecret' }
2305
+ });
2306
+
2307
+ // Set DATABASE_URL from new secret
2308
+ definition.provider.environment.DATABASE_URL = {
2309
+ 'Fn::Sub': [
2310
+ 'postgresql://${Username}:${Password}@${Endpoint}:${Port}/${DatabaseName}',
2311
+ {
2312
+ Username: { 'Fn::Sub': '{{resolve:secretsmanager:${FriggDatabaseSecret}:SecretString:username}}' },
2313
+ Password: { 'Fn::Sub': '{{resolve:secretsmanager:${FriggDatabaseSecret}:SecretString:password}}' },
2314
+ Endpoint: discoveredResources.aurora.endpoint,
2315
+ Port: discoveredResources.aurora.port,
2316
+ DatabaseName: dbConfig.databaseName || 'frigg_db'
2317
+ }
2318
+ ]
2319
+ };
2320
+
2321
+ console.log('✅ Self-healing configuration complete:');
2322
+ console.log(' - New secret will be created with auto-generated password');
2323
+ console.log(' - SecretTargetAttachment will automatically update cluster password');
2324
+ console.log(' - No manual password sync required!');
2325
+ } else {
2326
+ throw new Error(
2327
+ 'No database secret found. Options:\n' +
2328
+ ' 1. Provide secretArn in database.postgres configuration\n' +
2329
+ ' 2. Ensure Secrets Manager secret exists\n' +
2330
+ ' 3. Enable self-healing: set database.postgres.selfHeal to true (for Frigg-managed clusters only)'
2331
+ );
2332
+ }
2333
+
2334
+ // Set DB_TYPE for Prisma client selection
2335
+ definition.provider.environment.DB_TYPE = 'postgresql';
2336
+
2337
+ console.log('✅ Existing Aurora cluster configured');
2338
+ };
2339
+
2340
+ const useDiscoveredAurora = (definition, AppDefinition, discoveredResources) => {
2341
+ console.log(`🔍 Using discovered Aurora cluster: ${discoveredResources.aurora.clusterIdentifier}`);
2342
+ useExistingAurora(definition, AppDefinition, discoveredResources);
2343
+ };
2344
+
2345
+ const configurePostgres = (definition, AppDefinition, discoveredResources) => {
2346
+ if (!AppDefinition.database?.postgres?.enable) {
2347
+ return;
2348
+ }
2349
+
2350
+ // Validate VPC is enabled (required for Aurora deployment)
2351
+ if (!AppDefinition.vpc?.enable) {
2352
+ throw new Error(
2353
+ 'Aurora PostgreSQL requires VPC deployment. ' +
2354
+ 'Set vpc.enable to true in your app definition.'
2355
+ );
2356
+ }
2357
+
2358
+ // Validate private subnets exist (Aurora requires at least 2 subnets in different AZs)
2359
+ // Skip validation if VPC management is 'create-new' (subnets will be created)
2360
+ const vpcManagement = AppDefinition.vpc?.management || 'discover';
2361
+ if (vpcManagement !== 'create-new' && (!discoveredResources.privateSubnetId1 || !discoveredResources.privateSubnetId2)) {
2362
+ throw new Error(
2363
+ 'Aurora PostgreSQL requires at least 2 private subnets in different availability zones. ' +
2364
+ 'No private subnets were discovered in your VPC. ' +
2365
+ 'Please create private subnets or use VPC management mode "create-new".'
2366
+ );
2367
+ }
2368
+
2369
+ const dbConfig = AppDefinition.database.postgres;
2370
+ const management = dbConfig.management || 'discover';
2371
+
2372
+ console.log(`\n🐘 PostgreSQL Management Mode: ${management}`);
2373
+
2374
+ if (management === 'create-new' || discoveredResources.aurora?.needsCreation) {
2375
+ createAuroraInfrastructure(definition, AppDefinition, discoveredResources);
2376
+ } else if (management === 'use-existing') {
2377
+ if (!discoveredResources.aurora?.clusterIdentifier && !dbConfig.clusterIdentifier) {
2378
+ throw new Error('PostgreSQL management is set to "use-existing" but no clusterIdentifier was found or provided');
2379
+ }
2380
+ useExistingAurora(definition, AppDefinition, discoveredResources);
2381
+ } else {
2382
+ // discover mode
2383
+ if (discoveredResources.aurora?.clusterIdentifier) {
2384
+ useDiscoveredAurora(definition, AppDefinition, discoveredResources);
2385
+ } else {
2386
+ throw new Error('No Aurora cluster found in discovery mode. Set management to "create-new" or provide clusterIdentifier with "use-existing".');
2387
+ }
2388
+ }
2389
+ };
2390
+
1915
2391
  const configureSsm = (definition, AppDefinition) => {
1916
2392
  if (AppDefinition.ssm?.enable !== true) {
1917
2393
  return;
@@ -1949,19 +2425,31 @@ const attachIntegrations = (definition, AppDefinition) => {
1949
2425
  `Processing ${AppDefinition.integrations.length} integrations...`
1950
2426
  );
1951
2427
 
2428
+ // Get the functionPackageConfig from the definition (defined in createBaseDefinition)
2429
+ const functionPackageConfig = {
2430
+ exclude: [
2431
+ 'node_modules/aws-sdk/**',
2432
+ 'node_modules/@aws-sdk/**',
2433
+ 'node_modules/@prisma/**',
2434
+ 'node_modules/.prisma/**',
2435
+ 'node_modules/prisma/**',
2436
+ 'node_modules/@friggframework/core/generated/**',
2437
+ ],
2438
+ };
2439
+
1952
2440
  for (const integration of AppDefinition.integrations) {
1953
2441
  if (!integration?.Definition?.name) {
1954
2442
  throw new Error('Invalid integration: missing Definition or name');
1955
2443
  }
1956
2444
 
1957
2445
  const integrationName = integration.Definition.name;
1958
- const queueReference = `${
1959
- integrationName.charAt(0).toUpperCase() + integrationName.slice(1)
1960
- }Queue`;
2446
+ const queueReference = `${integrationName.charAt(0).toUpperCase() + integrationName.slice(1)
2447
+ }Queue`;
1961
2448
  const queueName = `\${self:service}--\${self:provider.stage}-${queueReference}`;
1962
2449
 
1963
2450
  definition.functions[integrationName] = {
1964
2451
  handler: `node_modules/@friggframework/core/handlers/routers/integration-defined-routers.handlers.${integrationName}.handler`,
2452
+ package: functionPackageConfig,
1965
2453
  events: [
1966
2454
  {
1967
2455
  httpApi: {
@@ -1990,6 +2478,7 @@ const attachIntegrations = (definition, AppDefinition) => {
1990
2478
  const queueWorkerName = `${integrationName}QueueWorker`;
1991
2479
  definition.functions[queueWorkerName] = {
1992
2480
  handler: `node_modules/@friggframework/core/handlers/workers/integration-defined-workers.handlers.${integrationName}.queueWorker`,
2481
+ package: functionPackageConfig,
1993
2482
  reservedConcurrency: 5,
1994
2483
  events: [
1995
2484
  {
@@ -2013,10 +2502,7 @@ const attachIntegrations = (definition, AppDefinition) => {
2013
2502
 
2014
2503
  // Add webhook handler if enabled
2015
2504
  const webhookConfig = integration.Definition.webhooks;
2016
- if (
2017
- webhookConfig &&
2018
- (webhookConfig === true || webhookConfig.enabled === true)
2019
- ) {
2505
+ if (webhookConfig && (webhookConfig === true || webhookConfig.enabled === true)) {
2020
2506
  const webhookFunctionName = `${integrationName}Webhook`;
2021
2507
 
2022
2508
  definition.functions[webhookFunctionName] = {
@@ -2056,9 +2542,44 @@ const configureWebsockets = (definition, AppDefinition) => {
2056
2542
  };
2057
2543
  };
2058
2544
 
2545
+ /**
2546
+ * Ensure Prisma Lambda Layer exists
2547
+ * Automatically builds the layer if it doesn't exist in the project root
2548
+ * @param {Object} databaseConfig - Database configuration from AppDefinition.database
2549
+ */
2550
+ async function ensurePrismaLayerExists(databaseConfig = {}) {
2551
+ const projectRoot = process.cwd();
2552
+ const layerPath = path.join(projectRoot, 'layers/prisma');
2553
+
2554
+ // Check if layer already exists
2555
+ if (fs.existsSync(layerPath)) {
2556
+ console.log('✓ Prisma Lambda Layer already exists at', layerPath);
2557
+ return;
2558
+ }
2559
+
2560
+ // Layer doesn't exist - build it automatically
2561
+ console.log('📦 Prisma Lambda Layer not found - building automatically...');
2562
+ console.log(' Building layer with CLI (used by all functions including dbMigrate)');
2563
+ console.log(' This may take a minute on first deployment.\n');
2564
+
2565
+ try {
2566
+ // Build layer WITH CLI (includeCLI = true) - all functions use same layer
2567
+ await buildPrismaLayer(databaseConfig, true);
2568
+ console.log('✓ Prisma Lambda Layer built successfully\n');
2569
+ } catch (error) {
2570
+ console.error('✗ Failed to build Prisma Lambda Layer:', error.message);
2571
+ console.error(' You may need to run: npm install @friggframework/core\n');
2572
+ throw error;
2573
+ }
2574
+ }
2575
+
2059
2576
  const composeServerlessDefinition = async (AppDefinition) => {
2060
2577
  console.log('composeServerlessDefinition', AppDefinition);
2061
2578
 
2579
+ // Ensure Prisma layer exists before generating serverless config
2580
+ // Pass database config so layer only includes needed database clients
2581
+ await ensurePrismaLayerExists(AppDefinition.database || {});
2582
+
2062
2583
  const discoveredResources = await gatherDiscoveredResources(AppDefinition);
2063
2584
  const appEnvironmentVars = getAppEnvironmentVars(AppDefinition);
2064
2585
  const definition = createBaseDefinition(
@@ -2080,6 +2601,7 @@ const composeServerlessDefinition = async (AppDefinition) => {
2080
2601
  if (!isLocalBuild) {
2081
2602
  applyKmsConfiguration(definition, AppDefinition, discoveredResources);
2082
2603
  configureVpc(definition, AppDefinition, discoveredResources);
2604
+ configurePostgres(definition, AppDefinition, discoveredResources);
2083
2605
  configureSsm(definition, AppDefinition);
2084
2606
  } else {
2085
2607
  console.log(