@friggframework/devtools 2.0.0-next.27 → 2.0.0-next.29

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.
Files changed (34) hide show
  1. package/frigg-cli/build-command/index.js +4 -2
  2. package/frigg-cli/deploy-command/index.js +5 -2
  3. package/frigg-cli/generate-iam-command.js +115 -0
  4. package/frigg-cli/index.js +11 -1
  5. package/infrastructure/AWS-DISCOVERY-TROUBLESHOOTING.md +245 -0
  6. package/infrastructure/AWS-IAM-CREDENTIAL-NEEDS.md +596 -0
  7. package/infrastructure/DEPLOYMENT-INSTRUCTIONS.md +268 -0
  8. package/infrastructure/GENERATE-IAM-DOCS.md +253 -0
  9. package/infrastructure/IAM-POLICY-TEMPLATES.md +176 -0
  10. package/infrastructure/README-TESTING.md +332 -0
  11. package/infrastructure/README.md +421 -0
  12. package/infrastructure/WEBSOCKET-CONFIGURATION.md +105 -0
  13. package/infrastructure/__tests__/fixtures/mock-aws-resources.js +391 -0
  14. package/infrastructure/__tests__/helpers/test-utils.js +277 -0
  15. package/infrastructure/aws-discovery.js +568 -0
  16. package/infrastructure/aws-discovery.test.js +373 -0
  17. package/infrastructure/build-time-discovery.js +206 -0
  18. package/infrastructure/build-time-discovery.test.js +375 -0
  19. package/infrastructure/create-frigg-infrastructure.js +2 -2
  20. package/infrastructure/frigg-deployment-iam-stack.yaml +379 -0
  21. package/infrastructure/iam-generator.js +687 -0
  22. package/infrastructure/iam-generator.test.js +169 -0
  23. package/infrastructure/iam-policy-basic.json +212 -0
  24. package/infrastructure/iam-policy-full.json +282 -0
  25. package/infrastructure/integration.test.js +383 -0
  26. package/infrastructure/run-discovery.js +110 -0
  27. package/infrastructure/serverless-template.js +537 -212
  28. package/infrastructure/serverless-template.test.js +541 -0
  29. package/management-ui/dist/assets/FriggLogo-B7Xx8ZW1.svg +1 -0
  30. package/management-ui/dist/assets/index-BA21WgFa.js +1221 -0
  31. package/management-ui/dist/assets/index-CbM64Oba.js +1221 -0
  32. package/management-ui/dist/assets/index-CkvseXTC.css +1 -0
  33. package/management-ui/dist/index.html +14 -0
  34. package/package.json +9 -5
@@ -1,7 +1,26 @@
1
1
  const path = require('path');
2
2
  const fs = require('fs');
3
+ const { AWSDiscovery } = require('./aws-discovery');
3
4
 
4
- // Function to find the actual path to node_modules
5
+ /**
6
+ * Check if AWS discovery should run based on AppDefinition
7
+ * @param {Object} AppDefinition - Application definition
8
+ * @returns {boolean} True if discovery should run
9
+ */
10
+ const shouldRunDiscovery = (AppDefinition) => {
11
+ return AppDefinition.vpc?.enable === true ||
12
+ AppDefinition.encryption?.useDefaultKMSForFieldLevelEncryption === true ||
13
+ AppDefinition.ssm?.enable === true;
14
+ };
15
+
16
+ /**
17
+ * Find the actual path to node_modules directory
18
+ * Tries multiple methods to locate node_modules:
19
+ * 1. Traversing up from current directory
20
+ * 2. Using npm root command
21
+ * 3. Looking for package.json and adjacent node_modules
22
+ * @returns {string} Path to node_modules directory
23
+ */
5
24
  const findNodeModulesPath = () => {
6
25
  try {
7
26
  // Method 1: Try to find node_modules by traversing up from current directory
@@ -75,7 +94,18 @@ const findNodeModulesPath = () => {
75
94
  }
76
95
  };
77
96
 
78
- // Function to modify handler paths to point to the correct node_modules
97
+ /**
98
+ * Modify handler paths to point to the correct node_modules location
99
+ * Only modifies paths when running in offline mode
100
+ * @param {Object} functions - Serverless functions configuration object
101
+ * @returns {Object} Modified functions object with updated handler paths
102
+ */
103
+ /**
104
+ * Modify handler paths to point to the correct node_modules location
105
+ * Only modifies paths when running in offline mode
106
+ * @param {Object} functions - Serverless functions configuration object
107
+ * @returns {Object} Modified functions object with updated handler paths
108
+ */
79
109
  const modifyHandlerPaths = (functions) => {
80
110
  // Check if we're running in offline mode
81
111
  const isOffline = process.argv.includes('offline');
@@ -94,7 +124,10 @@ const modifyHandlerPaths = (functions) => {
94
124
  const functionDef = modifiedFunctions[functionName];
95
125
  if (functionDef?.handler?.includes('node_modules/')) {
96
126
  // Replace node_modules/ with the actual path to node_modules/
97
- functionDef.handler = functionDef.handler.replace('node_modules/', '../node_modules/');
127
+ const relativePath = path.relative(process.cwd(), nodeModulesPath);
128
+ functionDef.handler = functionDef.handler.replace('node_modules/', `${relativePath}/`);
129
+ const relativePath = path.relative(process.cwd(), nodeModulesPath);
130
+ functionDef.handler = functionDef.handler.replace('node_modules/', `${relativePath}/`);
98
131
  console.log(`Updated handler for ${functionName}: ${functionDef.handler}`);
99
132
  }
100
133
  }
@@ -102,7 +135,22 @@ const modifyHandlerPaths = (functions) => {
102
135
  return modifiedFunctions;
103
136
  };
104
137
 
105
- // Helper function to create VPC infrastructure resources
138
+ /**
139
+ * Create VPC infrastructure resources for CloudFormation
140
+ * Creates VPC, subnets, NAT gateway, route tables, and security groups
141
+ * @param {Object} AppDefinition - Application definition object
142
+ * @param {Object} AppDefinition.vpc - VPC configuration
143
+ * @param {string} [AppDefinition.vpc.cidrBlock='10.0.0.0/16'] - CIDR block for VPC
144
+ * @returns {Object} CloudFormation resources for VPC infrastructure
145
+ */
146
+ /**
147
+ * Create VPC infrastructure resources for CloudFormation
148
+ * Creates VPC, subnets, NAT gateway, route tables, and security groups
149
+ * @param {Object} AppDefinition - Application definition object
150
+ * @param {Object} AppDefinition.vpc - VPC configuration
151
+ * @param {string} [AppDefinition.vpc.cidrBlock='10.0.0.0/16'] - CIDR block for VPC
152
+ * @returns {Object} CloudFormation resources for VPC infrastructure
153
+ */
106
154
  const createVPCInfrastructure = (AppDefinition) => {
107
155
  const vpcResources = {
108
156
  // VPC
@@ -292,13 +340,6 @@ const createVPCInfrastructure = (AppDefinition) => {
292
340
  CidrIp: '0.0.0.0/0',
293
341
  Description: 'HTTP outbound'
294
342
  },
295
- {
296
- IpProtocol: 'tcp',
297
- FromPort: 27017,
298
- ToPort: 27017,
299
- CidrIp: '0.0.0.0/0',
300
- Description: 'MongoDB Atlas TLS outbound'
301
- },
302
343
  {
303
344
  IpProtocol: 'tcp',
304
345
  FromPort: 53,
@@ -412,15 +453,57 @@ const createVPCInfrastructure = (AppDefinition) => {
412
453
  return vpcResources;
413
454
  };
414
455
 
415
- const composeServerlessDefinition = (AppDefinition) => {
416
- // Define CORS configuration to be used across all endpoints
417
- const corsConfig = {
418
- origin: '*',
419
- headers: '*',
420
- methods: ['ANY'],
421
- allowCredentials: false,
422
- };
423
-
456
+ /**
457
+ * Compose a complete serverless framework configuration from app definition
458
+ * @param {Object} AppDefinition - Application definition object
459
+ * @param {string} [AppDefinition.name] - Application name
460
+ * @param {string} [AppDefinition.provider='aws'] - Cloud provider
461
+ * @param {Array} AppDefinition.integrations - Array of integration definitions
462
+ * @param {Object} [AppDefinition.vpc] - VPC configuration
463
+ * @param {Object} [AppDefinition.encryption] - KMS encryption configuration
464
+ * @param {Object} [AppDefinition.ssm] - SSM parameter store configuration
465
+ * @param {Object} [AppDefinition.websockets] - WebSocket configuration
466
+ * @param {boolean} [AppDefinition.websockets.enable=false] - Enable WebSocket support for live update streaming
467
+ * @returns {Object} Complete serverless framework configuration
468
+ */
469
+ const composeServerlessDefinition = async (AppDefinition) => {
470
+ // Store discovered resources
471
+ let discoveredResources = {};
472
+
473
+ // Run AWS discovery if needed
474
+ if (shouldRunDiscovery(AppDefinition)) {
475
+ console.log('🔍 Running AWS resource discovery for serverless template...');
476
+ try {
477
+ const region = process.env.AWS_REGION || 'us-east-1';
478
+ const discovery = new AWSDiscovery(region);
479
+
480
+ const config = {
481
+ vpc: AppDefinition.vpc || {},
482
+ encryption: AppDefinition.encryption || {},
483
+ ssm: AppDefinition.ssm || {}
484
+ };
485
+
486
+ discoveredResources = await discovery.discoverResources(config);
487
+
488
+ console.log('✅ AWS discovery completed successfully!');
489
+ if (discoveredResources.defaultVpcId) {
490
+ console.log(` VPC: ${discoveredResources.defaultVpcId}`);
491
+ }
492
+ if (discoveredResources.privateSubnetId1 && discoveredResources.privateSubnetId2) {
493
+ console.log(` Subnets: ${discoveredResources.privateSubnetId1}, ${discoveredResources.privateSubnetId2}`);
494
+ }
495
+ if (discoveredResources.defaultSecurityGroupId) {
496
+ console.log(` Security Group: ${discoveredResources.defaultSecurityGroupId}`);
497
+ }
498
+ if (discoveredResources.defaultKmsKeyId) {
499
+ console.log(` KMS Key: ${discoveredResources.defaultKmsKeyId}`);
500
+ }
501
+ } catch (error) {
502
+ console.error('❌ AWS discovery failed:', error.message);
503
+ throw new Error(`AWS discovery failed: ${error.message}`);
504
+ }
505
+ }
506
+
424
507
  const definition = {
425
508
  frameworkVersion: '>=3.17.0',
426
509
  service: AppDefinition.name || 'create-frigg-app',
@@ -433,11 +516,28 @@ const composeServerlessDefinition = (AppDefinition) => {
433
516
  name: AppDefinition.provider || 'aws',
434
517
  runtime: 'nodejs20.x',
435
518
  timeout: 30,
436
- region: 'us-east-1',
519
+ region: process.env.AWS_REGION || 'us-east-1',
520
+ region: process.env.AWS_REGION || 'us-east-1',
437
521
  stage: '${opt:stage}',
438
522
  environment: {
439
- STAGE: '${opt:stage}',
523
+ STAGE: '${opt:stage, "dev"}',
440
524
  AWS_NODEJS_CONNECTION_REUSE_ENABLED: 1,
525
+ // Add discovered resources to environment if available
526
+ ...(discoveredResources.defaultVpcId && { AWS_DISCOVERY_VPC_ID: discoveredResources.defaultVpcId }),
527
+ ...(discoveredResources.defaultSecurityGroupId && { AWS_DISCOVERY_SECURITY_GROUP_ID: discoveredResources.defaultSecurityGroupId }),
528
+ ...(discoveredResources.privateSubnetId1 && { AWS_DISCOVERY_SUBNET_ID_1: discoveredResources.privateSubnetId1 }),
529
+ ...(discoveredResources.privateSubnetId2 && { AWS_DISCOVERY_SUBNET_ID_2: discoveredResources.privateSubnetId2 }),
530
+ ...(discoveredResources.publicSubnetId && { AWS_DISCOVERY_PUBLIC_SUBNET_ID: discoveredResources.publicSubnetId }),
531
+ ...(discoveredResources.defaultRouteTableId && { AWS_DISCOVERY_ROUTE_TABLE_ID: discoveredResources.defaultRouteTableId }),
532
+ ...(discoveredResources.defaultKmsKeyId && { AWS_DISCOVERY_KMS_KEY_ID: discoveredResources.defaultKmsKeyId }),
533
+ // Add discovered resources to environment if available
534
+ ...(discoveredResources.defaultVpcId && { AWS_DISCOVERY_VPC_ID: discoveredResources.defaultVpcId }),
535
+ ...(discoveredResources.defaultSecurityGroupId && { AWS_DISCOVERY_SECURITY_GROUP_ID: discoveredResources.defaultSecurityGroupId }),
536
+ ...(discoveredResources.privateSubnetId1 && { AWS_DISCOVERY_SUBNET_ID_1: discoveredResources.privateSubnetId1 }),
537
+ ...(discoveredResources.privateSubnetId2 && { AWS_DISCOVERY_SUBNET_ID_2: discoveredResources.privateSubnetId2 }),
538
+ ...(discoveredResources.publicSubnetId && { AWS_DISCOVERY_PUBLIC_SUBNET_ID: discoveredResources.publicSubnetId }),
539
+ ...(discoveredResources.defaultRouteTableId && { AWS_DISCOVERY_ROUTE_TABLE_ID: discoveredResources.defaultRouteTableId }),
540
+ ...(discoveredResources.defaultKmsKeyId && { AWS_DISCOVERY_KMS_KEY_ID: discoveredResources.defaultKmsKeyId }),
441
541
  },
442
542
  iamRoleStatements: [
443
543
  {
@@ -470,6 +570,17 @@ const composeServerlessDefinition = (AppDefinition) => {
470
570
  ],
471
571
  }
472
572
  ],
573
+ httpApi: {
574
+ payload: '2.0',
575
+ cors: {
576
+ allowedOrigins: ['*'],
577
+ allowedHeaders: ['*'],
578
+ allowedMethods: ['*'],
579
+ allowCredentials: false,
580
+ },
581
+ name: '${opt:stage, "dev"}-${self:service}',
582
+ disableDefaultEndpoint: false,
583
+ }
473
584
  },
474
585
  plugins: [
475
586
  'serverless-jetpack',
@@ -488,7 +599,8 @@ const composeServerlessDefinition = (AppDefinition) => {
488
599
  autoCreate: false,
489
600
  apiVersion: '2012-11-05',
490
601
  endpoint: 'http://localhost:4566',
491
- region: 'us-east-1',
602
+ region: process.env.AWS_REGION || 'us-east-1',
603
+ region: process.env.AWS_REGION || 'us-east-1',
492
604
  accessKeyId: 'root',
493
605
  secretAccessKey: 'root',
494
606
  skipCacheInvalidation: false,
@@ -498,48 +610,25 @@ const composeServerlessDefinition = (AppDefinition) => {
498
610
  },
499
611
  },
500
612
  functions: {
501
- defaultWebsocket: {
502
- handler: 'node_modules/@friggframework/core/handlers/routers/websocket.handler',
503
- events: [
504
- {
505
- websocket: {
506
- route: '$connect',
507
- },
508
- },
509
- {
510
- websocket: {
511
- route: '$default',
512
- },
513
- },
514
- {
515
- websocket: {
516
- route: '$disconnect',
517
- },
518
- },
519
- ],
520
- },
521
613
  auth: {
522
614
  handler: 'node_modules/@friggframework/core/handlers/routers/auth.handler',
523
615
  events: [
524
616
  {
525
- http: {
617
+ httpApi: {
526
618
  path: '/api/integrations',
527
619
  method: 'ANY',
528
- cors: corsConfig,
529
620
  },
530
621
  },
531
622
  {
532
- http: {
623
+ httpApi: {
533
624
  path: '/api/integrations/{proxy+}',
534
625
  method: 'ANY',
535
- cors: corsConfig,
536
626
  },
537
627
  },
538
628
  {
539
- http: {
629
+ httpApi: {
540
630
  path: '/api/authorize',
541
631
  method: 'ANY',
542
- cors: corsConfig,
543
632
  },
544
633
  },
545
634
  ],
@@ -548,10 +637,9 @@ const composeServerlessDefinition = (AppDefinition) => {
548
637
  handler: 'node_modules/@friggframework/core/handlers/routers/user.handler',
549
638
  events: [
550
639
  {
551
- http: {
640
+ httpApi: {
552
641
  path: '/user/{proxy+}',
553
642
  method: 'ANY',
554
- cors: corsConfig,
555
643
  },
556
644
  },
557
645
  ],
@@ -560,17 +648,15 @@ const composeServerlessDefinition = (AppDefinition) => {
560
648
  handler: 'node_modules/@friggframework/core/handlers/routers/health.handler',
561
649
  events: [
562
650
  {
563
- http: {
651
+ httpApi: {
564
652
  path: '/health',
565
653
  method: 'GET',
566
- cors: corsConfig,
567
654
  },
568
655
  },
569
656
  {
570
- http: {
657
+ httpApi: {
571
658
  path: '/health/{proxy+}',
572
659
  method: 'GET',
573
- cors: corsConfig,
574
660
  },
575
661
  },
576
662
  ],
@@ -582,7 +668,8 @@ const composeServerlessDefinition = (AppDefinition) => {
582
668
  Type: 'AWS::SQS::Queue',
583
669
  Properties: {
584
670
  QueueName:
585
- 'internal-error-queue-${self:provider.stage}',
671
+ '${self:service}-internal-error-queue-${self:provider.stage}',
672
+ '${self:service}-internal-error-queue-${self:provider.stage}',
586
673
  MessageRetentionPeriod: 300,
587
674
  },
588
675
  },
@@ -648,16 +735,12 @@ const composeServerlessDefinition = (AppDefinition) => {
648
735
  AlarmActions: [{ Ref: 'InternalErrorBridgeTopic' }],
649
736
  Dimensions: [
650
737
  {
651
- Name: 'ApiName',
652
- Value: {
653
- 'Fn::Join': [
654
- '-',
655
- [
656
- '${self:provider.stage}',
657
- '${self:service}',
658
- ],
659
- ],
660
- },
738
+ Name: 'ApiId',
739
+ Value: { Ref: 'HttpApi' },
740
+ },
741
+ {
742
+ Name: 'Stage',
743
+ Value: '${self:provider.stage}',
661
744
  },
662
745
  ],
663
746
  },
@@ -666,76 +749,7 @@ const composeServerlessDefinition = (AppDefinition) => {
666
749
  },
667
750
  };
668
751
 
669
- // Configure BASE_URL based on custom domain or API Gateway
670
- if (process.env.CUSTOM_DOMAIN) {
671
-
672
- // Configure custom domain
673
- definition.custom.customDomain = {
674
- domainName: process.env.CUSTOM_DOMAIN,
675
- basePath: process.env.CUSTOM_BASE_PATH || '',
676
- stage: '${self:provider.stage}',
677
- createRoute53Record: process.env.CREATE_ROUTE53_RECORD !== 'false', // Default true
678
- certificateName: process.env.CERTIFICATE_NAME || process.env.CUSTOM_DOMAIN,
679
- endpointType: process.env.ENDPOINT_TYPE || 'edge', // edge, regional, or private
680
- securityPolicy: process.env.SECURITY_POLICY || 'tls_1_2',
681
- apiType: 'rest',
682
- autoDomain: process.env.AUTO_DOMAIN === 'true', // Auto create domain if it doesn't exist
683
- };
684
-
685
- // Set BASE_URL to custom domain
686
- definition.provider.environment.BASE_URL = `https://${process.env.CUSTOM_DOMAIN}`;
687
- } else {
688
- // Default BASE_URL using API Gateway generated URL
689
- definition.provider.environment.BASE_URL = {
690
- 'Fn::Join': [
691
- '',
692
- [
693
- 'https://',
694
- { Ref: 'ApiGatewayRestApi' },
695
- '.execute-api.',
696
- { Ref: 'AWS::Region' },
697
- '.amazonaws.com/',
698
- '${self:provider.stage}',
699
- ],
700
- ],
701
- };
702
- }
703
-
704
- // REDIRECT_PATH is required for OAuth integrations
705
- if (!process.env.REDIRECT_PATH) {
706
- throw new Error(
707
- 'REDIRECT_PATH environment variable is required. ' +
708
- 'Please set REDIRECT_PATH in your .env file (e.g., REDIRECT_PATH=/oauth/callback)'
709
- );
710
- }
711
-
712
- // Set REDIRECT_URI based on domain configuration
713
- if (process.env.CUSTOM_DOMAIN) {
714
- definition.provider.environment.REDIRECT_URI = `https://${process.env.CUSTOM_DOMAIN}${process.env.REDIRECT_PATH}`;
715
- } else {
716
- definition.provider.environment.REDIRECT_URI = {
717
- 'Fn::Join': [
718
- '',
719
- [
720
- 'https://',
721
- { Ref: 'ApiGatewayRestApi' },
722
- '.execute-api.',
723
- { Ref: 'AWS::Region' },
724
- '.amazonaws.com/',
725
- '${self:provider.stage}',
726
- process.env.REDIRECT_PATH,
727
- ],
728
- ],
729
- };
730
- }
731
-
732
- // Add REDIRECT_URI to CloudFormation outputs
733
- definition.resources.Outputs = {
734
- RedirectURI: {
735
- Description: 'OAuth Redirect URI to register with providers',
736
- Value: definition.provider.environment.REDIRECT_URI,
737
- },
738
- };
752
+
739
753
 
740
754
  // KMS Configuration based on App Definition
741
755
  if (AppDefinition.encryption?.useDefaultKMSForFieldLevelEncryption === true) {
@@ -768,36 +782,17 @@ const composeServerlessDefinition = (AppDefinition) => {
768
782
  definition.provider.environment.KMS_KEY_ARN = { 'Fn::GetAtt': ['FriggKMSKey', 'Arn'] };
769
783
 
770
784
  definition.plugins.push('serverless-kms-grants');
771
- definition.custom.kmsGrants = { kmsKeyId: { 'Fn::GetAtt': ['FriggKMSKey', 'Arn'] } };
785
+
786
+ // Configure KMS grants with discovered default key
787
+ // Configure KMS grants with discovered default key
788
+ definition.custom.kmsGrants = {
789
+ kmsKeyId: discoveredResources.defaultKmsKeyId || '${env:AWS_DISCOVERY_KMS_KEY_ID}'
790
+ kmsKeyId: discoveredResources.defaultKmsKeyId || '${env:AWS_DISCOVERY_KMS_KEY_ID}'
791
+ };
772
792
  }
773
793
 
774
794
  // VPC Configuration based on App Definition
775
795
  if (AppDefinition.vpc?.enable === true) {
776
- // Create VPC config from App Definition or use auto-created resources
777
- const vpcConfig = {};
778
-
779
- if (AppDefinition.vpc.securityGroupIds) {
780
- // User provided custom security groups
781
- vpcConfig.securityGroupIds = AppDefinition.vpc.securityGroupIds;
782
- } else {
783
- // Use auto-created security group
784
- vpcConfig.securityGroupIds = [{ Ref: 'FriggLambdaSecurityGroup' }];
785
- }
786
-
787
- if (AppDefinition.vpc.subnetIds) {
788
- // User provided custom subnets
789
- vpcConfig.subnetIds = AppDefinition.vpc.subnetIds;
790
- } else {
791
- // Use auto-created private subnets
792
- vpcConfig.subnetIds = [
793
- { Ref: 'FriggPrivateSubnet1' },
794
- { Ref: 'FriggPrivateSubnet2' }
795
- ];
796
- }
797
-
798
- // Set VPC config for Lambda functions
799
- definition.provider.vpc = vpcConfig;
800
-
801
796
  // Add VPC-related IAM permissions
802
797
  definition.provider.iamRoleStatements.push({
803
798
  Effect: 'Allow',
@@ -811,75 +806,405 @@ const composeServerlessDefinition = (AppDefinition) => {
811
806
  Resource: '*'
812
807
  });
813
808
 
814
- // Add VPC infrastructure resources to CloudFormation
815
- const vpcResources = createVPCInfrastructure(AppDefinition);
816
- Object.assign(definition.resources.Resources, vpcResources);
809
+ // Default approach: Use AWS Discovery to find existing VPC resources
810
+ if (AppDefinition.vpc.createNew === true) {
811
+ // Option 1: Create new VPC infrastructure (explicit opt-in)
812
+ const vpcConfig = {};
813
+ // Add VPC-related IAM permissions
814
+ definition.provider.iamRoleStatements.push({
815
+ Effect: 'Allow',
816
+ Action: [
817
+ 'ec2:CreateNetworkInterface',
818
+ 'ec2:DescribeNetworkInterfaces',
819
+ 'ec2:DeleteNetworkInterface',
820
+ 'ec2:AttachNetworkInterface',
821
+ 'ec2:DetachNetworkInterface'
822
+ ],
823
+ Resource: '*'
824
+ });
825
+
826
+ // Default approach: Use AWS Discovery to find existing VPC resources
827
+ if (AppDefinition.vpc.createNew === true) {
828
+ // Option 1: Create new VPC infrastructure (explicit opt-in)
829
+ const vpcConfig = {};
830
+
831
+ if (AppDefinition.vpc.securityGroupIds) {
832
+ // User provided custom security groups
833
+ vpcConfig.securityGroupIds = AppDefinition.vpc.securityGroupIds;
834
+ } else {
835
+ // Use auto-created security group
836
+ vpcConfig.securityGroupIds = [{ Ref: 'FriggLambdaSecurityGroup' }];
837
+ }
838
+ if (AppDefinition.vpc.securityGroupIds) {
839
+ // User provided custom security groups
840
+ vpcConfig.securityGroupIds = AppDefinition.vpc.securityGroupIds;
841
+ } else {
842
+ // Use auto-created security group
843
+ vpcConfig.securityGroupIds = [{ Ref: 'FriggLambdaSecurityGroup' }];
844
+ }
845
+
846
+ if (AppDefinition.vpc.subnetIds) {
847
+ // User provided custom subnets
848
+ vpcConfig.subnetIds = AppDefinition.vpc.subnetIds;
849
+ } else {
850
+ // Use auto-created private subnets
851
+ vpcConfig.subnetIds = [
852
+ { Ref: 'FriggPrivateSubnet1' },
853
+ { Ref: 'FriggPrivateSubnet2' }
854
+ ];
855
+ }
856
+ if (AppDefinition.vpc.subnetIds) {
857
+ // User provided custom subnets
858
+ vpcConfig.subnetIds = AppDefinition.vpc.subnetIds;
859
+ } else {
860
+ // Use auto-created private subnets
861
+ vpcConfig.subnetIds = [
862
+ { Ref: 'FriggPrivateSubnet1' },
863
+ { Ref: 'FriggPrivateSubnet2' }
864
+ ];
865
+ }
866
+
867
+ // Set VPC config for Lambda functions
868
+ definition.provider.vpc = vpcConfig;
869
+ // Set VPC config for Lambda functions
870
+ definition.provider.vpc = vpcConfig;
871
+
872
+ // Add VPC infrastructure resources to CloudFormation
873
+ const vpcResources = createVPCInfrastructure(AppDefinition);
874
+ Object.assign(definition.resources.Resources, vpcResources);
875
+ } else {
876
+ // Option 2: Use AWS Discovery (default behavior)
877
+ // VPC configuration using discovered or explicitly provided resources
878
+ const vpcConfig = {
879
+ securityGroupIds: AppDefinition.vpc.securityGroupIds ||
880
+ (discoveredResources.defaultSecurityGroupId ? [discoveredResources.defaultSecurityGroupId] : []),
881
+ subnetIds: AppDefinition.vpc.subnetIds ||
882
+ (discoveredResources.privateSubnetId1 && discoveredResources.privateSubnetId2 ?
883
+ [discoveredResources.privateSubnetId1, discoveredResources.privateSubnetId2] :
884
+ [])
885
+ };
886
+
887
+ // Set VPC config for Lambda functions only if we have valid subnet IDs
888
+ if (vpcConfig.subnetIds.length >= 2 && vpcConfig.securityGroupIds.length > 0) {
889
+ definition.provider.vpc = vpcConfig;
890
+
891
+ // Check if we have an existing NAT Gateway to use
892
+ if (!discoveredResources.existingNatGatewayId) {
893
+ // No existing NAT Gateway, create new resources
894
+
895
+ // Only create EIP if we don't have an existing one available
896
+ if (!discoveredResources.existingElasticIpAllocationId) {
897
+ definition.resources.Resources.FriggNATGatewayEIP = {
898
+ Type: 'AWS::EC2::EIP',
899
+ Properties: {
900
+ Domain: 'vpc',
901
+ Tags: [
902
+ { Key: 'Name', Value: '${self:service}-${self:provider.stage}-nat-eip' }
903
+ ]
904
+ }
905
+ };
906
+ }
907
+
908
+ definition.resources.Resources.FriggNATGateway = {
909
+ Type: 'AWS::EC2::NatGateway',
910
+ Properties: {
911
+ AllocationId: discoveredResources.existingElasticIpAllocationId ||
912
+ { 'Fn::GetAtt': ['FriggNATGatewayEIP', 'AllocationId'] },
913
+ SubnetId: discoveredResources.publicSubnetId || discoveredResources.privateSubnetId1, // Use first discovered subnet if no public subnet found
914
+ Tags: [
915
+ { Key: 'Name', Value: '${self:service}-${self:provider.stage}-nat-gateway' }
916
+ ]
917
+ }
918
+ };
919
+ }
920
+
921
+ // Create route table for Lambda subnets to use NAT Gateway
922
+ definition.resources.Resources.FriggLambdaRouteTable = {
923
+ Type: 'AWS::EC2::RouteTable',
924
+ Properties: {
925
+ VpcId: discoveredResources.defaultVpcId || { Ref: 'FriggVPC' },
926
+ Tags: [
927
+ { Key: 'Name', Value: '${self:service}-${self:provider.stage}-lambda-rt' }
928
+ ]
929
+ }
930
+ };
931
+
932
+ definition.resources.Resources.FriggNATRoute = {
933
+ Type: 'AWS::EC2::Route',
934
+ Properties: {
935
+ RouteTableId: { Ref: 'FriggLambdaRouteTable' },
936
+ DestinationCidrBlock: '0.0.0.0/0',
937
+ NatGatewayId: discoveredResources.existingNatGatewayId || { Ref: 'FriggNATGateway' }
938
+ }
939
+ };
940
+
941
+ // Associate Lambda subnets with NAT Gateway route table
942
+ definition.resources.Resources.FriggSubnet1RouteAssociation = {
943
+ Type: 'AWS::EC2::SubnetRouteTableAssociation',
944
+ Properties: {
945
+ SubnetId: vpcConfig.subnetIds[0],
946
+ RouteTableId: { Ref: 'FriggLambdaRouteTable' }
947
+ }
948
+ };
949
+
950
+ definition.resources.Resources.FriggSubnet2RouteAssociation = {
951
+ Type: 'AWS::EC2::SubnetRouteTableAssociation',
952
+ Properties: {
953
+ SubnetId: vpcConfig.subnetIds[1],
954
+ RouteTableId: { Ref: 'FriggLambdaRouteTable' }
955
+ }
956
+ };
957
+
958
+ // Add VPC endpoints for AWS service optimization (optional but recommended)
959
+ if (AppDefinition.vpc.enableVPCEndpoints !== false) {
960
+ definition.resources.Resources.VPCEndpointS3 = {
961
+ Type: 'AWS::EC2::VPCEndpoint',
962
+ Properties: {
963
+ VpcId: discoveredResources.defaultVpcId,
964
+ ServiceName: 'com.amazonaws.${self:provider.region}.s3',
965
+ VpcEndpointType: 'Gateway',
966
+ RouteTableIds: [{ Ref: 'FriggLambdaRouteTable' }]
967
+ }
968
+ };
969
+
970
+ definition.resources.Resources.VPCEndpointDynamoDB = {
971
+ Type: 'AWS::EC2::VPCEndpoint',
972
+ Properties: {
973
+ VpcId: discoveredResources.defaultVpcId,
974
+ ServiceName: 'com.amazonaws.${self:provider.region}.dynamodb',
975
+ VpcEndpointType: 'Gateway',
976
+ RouteTableIds: [{ Ref: 'FriggLambdaRouteTable' }]
977
+ }
978
+ };
979
+ }
980
+ }
981
+ }
982
+ }
983
+
984
+ // SSM Parameter Store Configuration based on App Definition
985
+ if (AppDefinition.ssm?.enable === true) {
986
+ // Add AWS Parameters and Secrets Lambda Extension layer
987
+ definition.provider.layers = [
988
+ 'arn:aws:lambda:${self:provider.region}:177933569100:layer:AWS-Parameters-and-Secrets-Lambda-Extension:11'
989
+ ];
990
+
991
+ // Add SSM IAM permissions
992
+ definition.provider.iamRoleStatements.push({
993
+ Effect: 'Allow',
994
+ Action: [
995
+ 'ssm:GetParameter',
996
+ 'ssm:GetParameters',
997
+ 'ssm:GetParametersByPath'
998
+ ],
999
+ Resource: [
1000
+ 'arn:aws:ssm:${self:provider.region}:*:parameter/${self:service}/${self:provider.stage}/*'
1001
+ ]
1002
+ 'ssm:GetParameter',
1003
+ 'ssm:GetParameters',
1004
+ 'ssm:GetParametersByPath'
1005
+ ],
1006
+ Resource: [
1007
+ 'arn:aws:ssm:${self:provider.region}:*:parameter/${self:service}/${self:provider.stage}/*'
1008
+ ]
1009
+ });
1010
+
1011
+ // Add environment variable for SSM parameter prefix
1012
+ definition.provider.environment.SSM_PARAMETER_PREFIX = '/${self:service}/${self:provider.stage}';
1013
+ // Add environment variable for SSM parameter prefix
1014
+ definition.provider.environment.SSM_PARAMETER_PREFIX = '/${self:service}/${self:provider.stage}';
817
1015
  }
818
1016
 
819
- // Add integration-specific functions and resources
1017
+ // Add integration-specific functions and resources
1018
+ if (AppDefinition.integrations && Array.isArray(AppDefinition.integrations)) {
820
1019
  for (const integration of AppDefinition.integrations) {
1020
+ if (!integration || !integration.Definition || !integration.Definition.name) {
1021
+ throw new Error('Invalid integration: missing Definition or name');
1022
+ }
821
1023
  const integrationName = integration.Definition.name;
1024
+ if (AppDefinition.integrations && Array.isArray(AppDefinition.integrations)) {
1025
+ for (const integration of AppDefinition.integrations) {
1026
+ if (!integration || !integration.Definition || !integration.Definition.name) {
1027
+ throw new Error('Invalid integration: missing Definition or name');
1028
+ }
1029
+ const integrationName = integration.Definition.name;
1030
+
1031
+ // Add function for the integration
1032
+ definition.functions[integrationName] = {
1033
+ handler: `node_modules/@friggframework/core/handlers/routers/integration-defined-routers.handlers.${integrationName}.handler`,
1034
+ events: [
1035
+ {
1036
+ httpApi: {
1037
+ path: `/api/${integrationName}-integration/{proxy+}`,
1038
+ method: 'ANY',
1039
+ },
1040
+ },
1041
+ ],
1042
+ };
1043
+
1044
+ // Add SQS Queue for the integration
1045
+ const queueReference = `${integrationName.charAt(0).toUpperCase() + integrationName.slice(1)
1046
+ }Queue`;
1047
+ const queueName = `\${self:service}--\${self:provider.stage}-${queueReference}`;
1048
+ definition.resources.Resources[queueReference] = {
1049
+ Type: 'AWS::SQS::Queue',
1050
+ Properties: {
1051
+ QueueName: `\${self:custom.${queueReference}}`,
1052
+ MessageRetentionPeriod: 60,
1053
+ VisibilityTimeout: 1800, // 30 minutes
1054
+ RedrivePolicy: {
1055
+ maxReceiveCount: 1,
1056
+ deadLetterTargetArn: {
1057
+ 'Fn::GetAtt': ['InternalErrorQueue', 'Arn'],
1058
+ },
1059
+ },
1060
+ },
1061
+ };
1062
+
1063
+ // Add Queue Worker for the integration
1064
+ const queueWorkerName = `${integrationName}QueueWorker`;
1065
+ definition.functions[queueWorkerName] = {
1066
+ handler: `node_modules/@friggframework/core/handlers/workers/integration-defined-workers.handlers.${integrationName}.queueWorker`,
1067
+ reservedConcurrency: 5,
1068
+ events: [
1069
+ {
1070
+ sqs: {
1071
+ arn: {
1072
+ 'Fn::GetAtt': [queueReference, 'Arn'],
1073
+ },
1074
+ batchSize: 1,
1075
+ },
1076
+ },
1077
+ ],
1078
+ timeout: 600,
1079
+ };
1080
+
1081
+ // Add Queue URL for the integration to the ENVironment variables
1082
+ definition.provider.environment = {
1083
+ ...definition.provider.environment,
1084
+ [`${integrationName.toUpperCase()}_QUEUE_URL`]: {
1085
+ Ref: queueReference,
1086
+ },
1087
+ };
1088
+ // Add Queue URL for the integration to the ENVironment variables
1089
+ definition.provider.environment = {
1090
+ ...definition.provider.environment,
1091
+ [`${integrationName.toUpperCase()}_QUEUE_URL`]: {
1092
+ Ref: queueReference,
1093
+ },
1094
+ };
1095
+
1096
+ definition.custom[queueReference] = queueName;
1097
+ }
1098
+ }
1099
+
1100
+ // Discovery has already run successfully at this point if needed
1101
+ // The discoveredResources object contains all the necessary AWS resources
1102
+
1103
+ // Add websocket function if enabled
1104
+ if (AppDefinition.websockets?.enable === true) {
1105
+ definition.functions.defaultWebsocket = {
1106
+ handler: 'node_modules/@friggframework/core/handlers/routers/websocket.handler',
1107
+ events: [
1108
+ {
1109
+ websocket: {
1110
+ route: '$connect',
1111
+ },
1112
+ },
1113
+ {
1114
+ websocket: {
1115
+ route: '$default',
1116
+ },
1117
+ },
1118
+ {
1119
+ websocket: {
1120
+ route: '$disconnect',
1121
+ },
1122
+ },
1123
+ ],
1124
+ };
1125
+ }
1126
+
1127
+ // Discovery has already run successfully at this point if needed
1128
+ // The discoveredResources object contains all the necessary AWS resources
1129
+
1130
+ // Add websocket function if enabled
1131
+ if (AppDefinition.websockets?.enable === true) {
1132
+ definition.functions.defaultWebsocket = {
1133
+ handler: 'node_modules/@friggframework/core/handlers/routers/websocket.handler',
1134
+ events: [
1135
+ {
1136
+ websocket: {
1137
+ route: '$connect',
1138
+ },
1139
+ },
1140
+ {
1141
+ websocket: {
1142
+ route: '$default',
1143
+ },
1144
+ },
1145
+ {
1146
+ websocket: {
1147
+ route: '$disconnect',
1148
+ },
1149
+ },
1150
+ ],
1151
+ };
1152
+ definition.custom[queueReference] = queueName;
1153
+ }
1154
+ }
1155
+
1156
+ // Discovery has already run successfully at this point if needed
1157
+ // The discoveredResources object contains all the necessary AWS resources
822
1158
 
823
- // Add function for the integration
824
- definition.functions[integrationName] = {
825
- handler: `node_modules/@friggframework/core/handlers/routers/integration-defined-routers.handlers.${integrationName}.handler`,
1159
+ // Add websocket function if enabled
1160
+ if (AppDefinition.websockets?.enable === true) {
1161
+ definition.functions.defaultWebsocket = {
1162
+ handler: 'node_modules/@friggframework/core/handlers/routers/websocket.handler',
826
1163
  events: [
827
1164
  {
828
- http: {
829
- path: `/api/${integrationName}-integration/{proxy+}`,
830
- method: 'ANY',
831
- cors: corsConfig,
1165
+ websocket: {
1166
+ route: '$connect',
832
1167
  },
833
1168
  },
834
- ],
835
- };
836
-
837
- // Add SQS Queue for the integration
838
- const queueReference = `${integrationName.charAt(0).toUpperCase() + integrationName.slice(1)
839
- }Queue`;
840
- const queueName = `\${self:service}--\${self:provider.stage}-${queueReference}`;
841
- definition.resources.Resources[queueReference] = {
842
- Type: 'AWS::SQS::Queue',
843
- Properties: {
844
- QueueName: `\${self:custom.${queueReference}}`,
845
- MessageRetentionPeriod: 60,
846
- VisibilityTimeout: 1800, // 30 minutes
847
- RedrivePolicy: {
848
- maxReceiveCount: 1,
849
- deadLetterTargetArn: {
850
- 'Fn::GetAtt': ['InternalErrorQueue', 'Arn'],
1169
+ {
1170
+ websocket: {
1171
+ route: '$default',
851
1172
  },
852
1173
  },
853
- },
1174
+ {
1175
+ websocket: {
1176
+ route: '$disconnect',
1177
+ },
1178
+ },
1179
+ ],
854
1180
  };
1181
+ }
855
1182
 
856
- // Add Queue Worker for the integration
857
- const queueWorkerName = `${integrationName}QueueWorker`;
858
- definition.functions[queueWorkerName] = {
859
- handler: `node_modules/@friggframework/core/handlers/workers/integration-defined-workers.handlers.${integrationName}.queueWorker`,
860
- reservedConcurrency: 5,
1183
+ // Discovery has already run successfully at this point if needed
1184
+ // The discoveredResources object contains all the necessary AWS resources
1185
+
1186
+ // Add websocket function if enabled
1187
+ if (AppDefinition.websockets?.enable === true) {
1188
+ definition.functions.defaultWebsocket = {
1189
+ handler: 'node_modules/@friggframework/core/handlers/routers/websocket.handler',
861
1190
  events: [
862
1191
  {
863
- sqs: {
864
- arn: {
865
- 'Fn::GetAtt': [queueReference, 'Arn'],
866
- },
867
- batchSize: 1,
1192
+ websocket: {
1193
+ route: '$connect',
1194
+ },
1195
+ },
1196
+ {
1197
+ websocket: {
1198
+ route: '$default',
1199
+ },
1200
+ },
1201
+ {
1202
+ websocket: {
1203
+ route: '$disconnect',
868
1204
  },
869
1205
  },
870
1206
  ],
871
- timeout: 600,
872
- };
873
-
874
- // Add Queue URL for the integration to the ENVironment variables
875
- definition.provider.environment = {
876
- ...definition.provider.environment,
877
- [`${integrationName.toUpperCase()}_QUEUE_URL`]: {
878
- Ref: queueReference,
879
- },
880
1207
  };
881
-
882
- definition.custom[queueReference] = queueName;
883
1208
  }
884
1209
 
885
1210
  // Modify handler paths to point to the correct node_modules location